멀티캠퍼스 데이터분석 4월 27일 수업 내용 — Flask 라우팅, Jinja2 템플릿, Chart.js, Bootstrap Dashboard, DB 연동 로그인
20260427/
├── querys/
│ └── user.py # SQL 쿼리문 모음
├── static/
│ ├── css/
│ │ ├── bootstrap.min.css # Bootstrap 5.3.8 CSS
│ │ └── dashboard.css # 커스텀 스타일
│ └── js/
│ ├── bootstrap.bundle.min.js # Bootstrap JS
│ ├── color-modes.js # 다크/라이트 모드 토글
│ └── dashboard.js # Chart.js 그래프
├── templates/
│ ├── index.html # 로그인 페이지
│ ├── main.html # 로그인 성공 후 메인 페이지
│ └── dashboard.html # Bootstrap Dashboard 예제
├── app.py # Flask 메인 서버
└── db.py # MyDB 클래스
💡 Flask 프로젝트 폴더 구조 규칙
templates/→ HTML 파일 저장 (Flask가 자동으로 인식)static/→ CSS, JS, 이미지 등 정적 파일 저장querys/→ SQL 쿼리문을 별도 모듈로 분리하여 관리
SQL 쿼리문들을 별도 모듈로 분리해서 관리합니다.
app.py에서 from querys import user 로 불러와 사용합니다.
# SQL query문들을 저장
# 회원 테이블 생성
create_query = """
CREATE TABLE IF NOT EXISTS user (
id VARCHAR(32) PRIMARY KEY,
password VARCHAR(32) NOT NULL,
name VARCHAR(32) NOT NULL
)
"""
# 로그인 조회 — id, password 모두 일치하는 데이터 존재 여부 확인
login_query = """
SELECT * FROM user
WHERE id = %s AND password = %s
"""
# 아이디 중복 체크
id_check_query = """
SELECT * FROM user
WHERE id = %s
"""
# 회원 가입
signup_query = """
INSERT INTO user VALUES (%s, %s, %s)
"""
💡 쿼리문을 별도 모듈로 분리하면
app.py가 깔끔해지고 유지보수가 편해집니다.
생성자에 기본값이 설정되어 있어 MyDB() 로 바로 로컬 DB 연결이 가능합니다.
import pymysql
class MyDB:
# 생성자 함수 (서버의 정보를 받아오기 위해 사용)
def __init__(self,
host = '127.0.0.1',
port = 3306,
user = 'root',
password = '1234',
db = 'multicam'):
# 서버의 정보를 인자로 받아와서 객체 내부에서 독립적인 변수에 저장
self.host = host
self.port = port
self.user = user
self.password = password
self.db = db
# 데이터베이스에 변화를 주는 함수
def commit(self):
try:
self.db_server.commit()
self.db_server.close()
del self.db_server
except:
print('DB 서버와 연결되어 있지 않습니다. sql_query()를 먼저 호출하세요.')
def sql_query(self, query, *datas):
try:
self.db_server
print('접속된 서버가 존재함')
except:
self.db_server = pymysql.connect(
host = self.host,
port = self.port,
user = self.user,
password = self.password,
db = self.db
)
# cursor 생성
cursor = self.db_server.cursor(pymysql.cursors.DictCursor)
try:
cursor.execute(query, datas)
if query.lstrip().lower().startswith('select'):
result = cursor.fetchall()
else:
result = 'Query OK!'
return result
except Exception as e:
print('query 실행 중 에러:', e)
# flask 프레임워크 안에서 특정 기능을 로드
from flask import Flask, render_template, request, redirect, url_for
from db import MyDB
from querys import user
import pandas as pd
app = Flask(__name__)
db = MyDB()
# 서버 시작 시 user 테이블 자동 생성
db.sql_query(user.create_query)
db.commit()
💡 Flask 주요 import
기능 설명 Flask웹 서버 클래스 render_templatetemplates/폴더의 HTML 파일 불러오기request유저가 보낸 데이터 접근 redirect특정 주소로 이동 url_for상대 경로 지정 (static 파일 경로 생성 등)
# / → index.html 렌더링
@app.route('/')
def index():
return render_template('index.html')
# Jinja2 템플릿 문법
# 1. {{ 변수명 }}: 파이썬에서 넘겨받은 데이터를 화면에 '출력'할 때 사용
# 2. {% 파이썬 코드 %}: code를 인식하여 조건에 맞는 경우만 html를 추가
# - {% if ... %} : 조건에 맞을 때만 특정 HTML 부분을 보여줌
# - {% for ... %} : 데이터를 반복하며 HTML 요소를 여러 개 생성
# /second → 단순 텍스트 반환
@app.route('/second')
def second():
return 'Second Page'
# GET /login → URL 쿼리스트링으로 데이터 수신 (하드코딩 체크)
# /login 주소를 생성
@app.route('/login')
def login():
# 유저가 보낸 데이터를 받아온다. (flask 프레임퉈크에서 기능을 불러와서 사용)
# get 방식으로 보낸 데이터를 확인
print(request.args)
# requests.args가 dict형태 -> 각각의 key 값으로 데이터를 추출
input_id = request.args['input_id']
input_pass = request.args['input_pass']
if input_id == 'admin' and input_pass == '1234':
return '로그인 성공'
else:
return 'ID, PASSWORD를 확인해주세요'
# POST /login2 → form 데이터로 수신, DB 조회 후 로그인 처리
# post 방식은 데이터를 숨겨서 보낸다 // 웹브라우저에서 주소창에 입력하여 확인이 불가능
@app.route('/login2', methods=['post'])
def login2():
_id = request.form['input_id']
_pass = request.form['input_pass']
# login_query에는 2개의 데이터가 필요(id, password)
sql_result = db.sql_query(user.login_query, _id, _pass)
# DB_server와의 연결을 종료
db.commit()
if sql_result:
# 리스트에서 특정 위치를 선택 -> sql_result[0] -> {'id': 'admin', 'password': '1234', 'name': 'kim'}
# 딕셔너리에서 특정 키를 선택 -> sql_result[0]['name'] -> 'kim'
sql_name = sql_result[0]['name']
# aapl.csv 로드 → 최근 10개 데이터 추출
df = pd.read_csv('../csv/aapl.csv')
df2 = df.tail(10)
# Chart.js용 x, y 데이터
# 데이터프레임에서 특정 컬럼을 하나 선택 -> Series(html에서 사용 불가능) -> list 형으로 변환
x_data = df2['Date'].tolist()
# y축 데이터 -> Adj Close 컬럼
y_data = df2['Adj Close'].tolist()
# 테이블용 컬럼명 & 데이터
# 테이블의 컬럼의 이름들을 따로 추출해서 list형태로 변환
th_data = df2.columns.tolist()
# th_data -> ['Date','Open',...]
td_data = df2.to_dict('records')
return render_template(
'main.html',
name = sql_name,
x = x_data,
y = y_data,
th_data = th_data,
td_data = td_data
)
else:
return redirect('/')
# /dashboard → Bootstrap Dashboard 페이지
@app.route('/dashboard')
def dashboard():
return render_template('dashboard.html')
# 웹 서버를 시작한다(구동한다) -> 함수의 return과 흡사 -> run() 함수 아래의 주소 값들은 적용되지 x
# run() 매개변수
# host 매개변수 -> 허용 가능 주소 목록( 기본값은 로컬의 pc에서만 접근 가능,
# 0.0.0.0으로 지정하면 모든 주소가 접근이 가능 )
# port 매개변수 -> 해당 웹서버의 포트 번호를 지정(5000번 기본값)
# debug 매개변수 -> 디버그 모드를 사용할 것인가? (기본값 : False)
# True 변경하면 개발 모드 변경 -> 파일은 수정이 되었을때 서버가 재시작
app.run(debug=True)
GET (/login) | POST (/login2) | |
|---|---|---|
| 데이터 위치 | URL 쿼리스트링 | request body |
| Flask 접근 | request.args['키'] | request.form['키'] |
| 보안 | URL에 노출됨 | 숨겨서 전송 |
| HTML 메서드 | method="get" | method="post" |
⚠️
app.run(debug=True)아래 코드는 실행되지 않습니다.
debug=True→ 파일 수정 시 서버 자동 재시작 (개발 모드)
GET 방식과 POST 방식 두 가지 로그인 폼을 구현합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>Hello Flask</h1>
<p>이 페이지는 메인 페이지 입니다.</p>
<!-- GET 방식 → /login 으로 전송 -->
<form action="/login" method="get">
<label>ID</label>
<input type="text" name="input_id">
<br>
<label>PASSWORD</label>
<input type="password" name="input_pass">
<br>
<input type="submit">
</form>
<h3>post 방식으로 데이터 보내기</h3>
<!-- POST 방식 → /login2 으로 전송 -->
<form action="/login2" method="post">
<label>ID</label>
<input type="text" name="input_id">
<br>
<label>PASSWORD</label>
<input type="password" name="input_pass">
<br>
<input type="submit">
</form>
</body>
</html>
💡 form 태그 주의사항
action→ 데이터를 보낼 주소method→ 전송 방식 (get/post)input의name속성 → 서버에서 데이터를 꺼낼 때 사용하는 key 값from으로 오타 내는 경우가 많으니 주의!
Jinja2 템플릿 문법으로 Python 변수를 HTML에 대입합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>Main Page</h1>
<!-- app.py에서 전달한 name 변수 대입 -->
<p>{{name}} 님 안녕하세요!</p>
<!-- Chart.js 그래프 영역 -->
<div>
<canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- 실제 그래프를 canvas에 그려주는 부분 -->
<script>
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'line',
data: {
labels: {{x | tojson}}, // x축 — Date 컬럼
datasets: [{
label: '# of Votes',
data: {{y | tojson}}, // y축 — Adj Close 컬럼
borderWidth: 1
}]
},
options: {}
});
</script>
<!-- 동적 테이블 -->
<table border=1>
<tr>
<!--
th_data -> ['Data','Open','High','Low','Close','Adj Close','Volumn']
반복문을 이용해서 작업을 하면 짧고 유동성있게 사용이 가능
python에서 for문은 들여쓰기를 통해 반복 실행 구문 선택
-->
<!-- 반복문의 시작 -->
{% for col in th_data %}
<th>{{col}}</th>
<!-- 반복문의 끝 -->
{% endfor %}
</tr>
<!--
td_data->
[{'Date' : xxx, 'Open' : xxx, 'High' : xxx, 'Low' : xxx, 'Close' : xxx, 'Adj Close' : xxx, 'Volume' : xxx},{}]
-->
{% for row in td_data %}
<tr>
{% for col in th_data %}
<td>{{row[col]}}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</body>
</html>
| 문법 | 설명 |
|---|---|
{{변수명}} | Python 변수를 HTML에 출력 |
{{변수 \| tojson}} | Python 데이터를 JSON 형식으로 변환 (JS에서 사용) |
{% for item in list %} | 반복문 시작 |
{% endfor %} | 반복문 끝 |
{% if 조건 %} | 조건문 시작 |
{% endif %} | 조건문 끝 |
💡
tolist()vsto_dict('records')
Series.tolist()→ Python 리스트로 변환 (Jinja2에서 사용 가능)df.to_dict('records')→[{컬럼명: 값, ...}, ...]형태로 변환 → 테이블 렌더링에 사용
Bootstrap 5.3 공식 예제 Dashboard 템플릿을 Flask에 적용합니다.
외부 CSS/JS 경로를 url_for('static', filename=...) 로 지정합니다.
<!-- Bootstrap 5.3.8 Examples의 dashboard 폴더에서 가져온 템플릿 -->
<!-- color-modes.js — head에서 먼저 로드 -->
<script src="{{url_for('static', filename='js/color-modes.js')}}"></script>
<!-- Bootstrap CSS -->
<link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet" />
<!-- 커스텀 CSS -->
<link href="{{url_for('static', filename='css/dashboard.css')}}" rel="stylesheet" />
<!-- Bootstrap JS (body 끝에) -->
<script src="{{url_for('static', filename='js/bootstrap.bundle.min.js')}}"></script>
<!-- Chart.js (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js"></script>
<!-- dashboard.js -->
<script src="{{url_for('static', filename='js/dashboard.js')}}"></script>
💡
url_for('static', filename='경로')사용 이유
HTML 파일에서 static 파일을 직접 경로로 쓰면 오류가 발생할 수 있습니다.
Flask의url_for()를 사용하면app.py위치를 기준으로 올바른 경로를 자동 생성합니다.
Bootstrap Dashboard의 사이드바, 네브바 스타일을 커스터마이징합니다.
/* 사이드바 nav-link 기본 스타일 */
.sidebar .nav-link {
font-size: .875rem;
font-weight: 500;
}
/* 활성화된 nav-link 색상 */
.sidebar .nav-link.active {
color: #2470dc;
}
/* 사이드바 소제목 */
.sidebar-heading {
font-size: .75rem;
}
/* 네브바 브랜드 영역 */
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
/* 반응형 — md 이상에서 사이드바 sticky */
@media (min-width: 768px) {
.sidebar .offcanvas-lg {
position: sticky;
top: 48px;
}
}
Bootstrap Dashboard의 라이트/다크/자동 테마를 전환하는 JS입니다.
localStorage에 선택한 테마를 저장해 페이지 재방문 시에도 유지됩니다.
(() => {
'use strict'
// localStorage에서 저장된 테마 불러오기 / 저장
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
// 시스템 설정 또는 저장값 기반으로 테마 결정
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) return storedTheme
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
// HTML에 data-bs-theme 속성 적용
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute(
'data-bs-theme',
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
)
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
// 시스템 다크모드 변경 감지
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
// 버튼 클릭 시 테마 변경
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()
Dashboard 페이지의 기본 라인 차트를 그립니다.
(() => {
'use strict'
const ctx = document.getElementById('myChart')
new Chart(ctx, {
type: 'line',
data: {
labels: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
datasets: [{
data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#007bff',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
},
options: {
plugins: {
legend: { display: false },
tooltip: { boxPadding: 3 }
}
}
})
})()
💡
main.html의 Chart.js는 Python에서 전달한 실시간 데이터를 사용하고,
dashboard.js의 Chart.js는 하드코딩된 샘플 데이터를 사용합니다.
cd C:\study\multicampus_practice\20260427
python app.py
서버가 정상 실행되면 아래와 같은 메시지가 출력됩니다.
* Serving Flask app 'app'
* Debug mode: on
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Debugger is active!

브라우저에서 127.0.0.1:5000 접속
↓
index.html 로그인 페이지 렌더링
↓
id/password 입력 후 제출
↓
GET 방식: 127.0.0.1:5000/login?input_id=syeom&input_pass=1234
POST 방식: /login2 (URL에 데이터 미노출)
↓
DB에서 user 테이블 조회
↓
로그인 성공 → main.html (그래프 + 테이블)
로그인 실패 → redirect('/') 로 돌아감



① 서버 실행 (터미널)
② 로그인 페이지 (127.0.0.1:5000)
로그인 폼에 id = syeom, password = 1234 입력 시
GET 방식이면 URL에 데이터가 노출됩니다.
127.0.0.1:5000/?input_id=syeom&input_pass=1234

③ DB 테이블 생성 확인 (MySQL Workbench)
서버 시작 시 user.create_query 가 자동 실행되어 user 테이블이 생성됩니다.
SELECT * FROM multicam.user;
-- admin / 1234 / kim
-- test / 0000 / park

④ Dashboard 페이지 (127.0.0.1:5000/dashboard)
Bootstrap 5.3 Dashboard 예제 템플릿이 Flask에 적용됩니다.
url_for('static', filename=...) 으로 CSS/JS 파일이 정상 로드되는지 확인합니다.
GET /static/js/color-modes.js 200
GET /static/css/bootstrap.min.css 200
GET /static/css/dashboard.css 200
GET /static/js/bootstrap.bundle.min.js 200
GET /static/js/dashboard.js 200

⚠️ 자주 발생하는 에러
BuildError: Could not build url for endpoint 'static' with values ['filenamge']
url_for('static', filename=...)에서filename을filenamge로 오타 낸 경우입니다.
철자를 정확히 확인하세요!
| 개념 | 설명 |
|---|---|
Flask(__name__) | Flask 앱 생성 |
@app.route('경로') | URL과 함수 연결 (데코레이터) |
render_template('파일.html', 변수=값) | HTML 렌더링 + 변수 전달 |
request.args['키'] | GET 방식 데이터 수신 |
request.form['키'] | POST 방식 데이터 수신 |
redirect('/') | 특정 주소로 이동 |
url_for('static', filename='경로') | static 파일 경로 생성 |
{{변수}} | Jinja2 변수 출력 |
{{변수 \| tojson}} | Python → JSON 변환 (JS 사용) |
{% for %} ~ {% endfor %} | Jinja2 반복문 |
df.tail(n) | 하위 n개 행 추출 |
Series.tolist() | Series → Python 리스트 |
df.to_dict('records') | DataFrame → 딕셔너리 리스트 |
app.run(debug=True) | 개발 서버 실행 (파일 수정 시 자동 재시작) |