윤영준 윤영준 04-24
now the server accepts more forms of data, like accelometer, gyroscope, orientation, and azimuth
@ae080f2cc4ed676021d2840c66ba634be62dec43
 
README.md (added)
+++ README.md
@@ -0,0 +1,1 @@
+# Daegu-Jeju-DIP
 
action.py (added)
+++ action.py
@@ -0,0 +1,296 @@
+
+from flask_restx import Resource, Namespace, fields
+from flask import Flask, request
+import os
+from database.database import DB
+import pandas as pd
+import jwt
+import datetime
+
+
+paths = os.getcwd()
+
+Action = Namespace(
+    name="Action",
+    description="노드 분석을 위해 사용하는 api.",
+)
+
+trip_log_model = Action.model('TripLog', {
+    'trip_id': fields.String(required=True, description='The ID of the trip (64 characters)'),
+    'trip_distance_m': fields.Float(required=True, description='Total distance traveled in meters'),
+    'trip_time_s': fields.Float(required=True, description='Total time of the trip in seconds'),
+    'abrupt_start_count': fields.Integer(required=True, description='Count of abrupt starts'),
+    'abrupt_stop_count': fields.Integer(required=True, description='Count of abrupt stops'),
+    'abrupt_acceleration_count': fields.Integer(required=True, description='Count of abrupt accelerations'),
+    'abrupt_deceleration_count': fields.Integer(required=True, description='Count of abrupt decelerations'),
+    'helmet_on': fields.Integer(required=True, description='Whether the helmet was worn during the trip, must be 0 or 1 with 1 is the helmet on.'),
+    'final_score': fields.Float(required=True, description='The final safety score for the trip')
+})
+
+history_request_model = Action.model(
+    'history_request', {
+        'user_id' : fields.String(required=True, description = 'The user ID that you want to query history')
+    }
+)
+
+
+@Action.route('/gps_update')
+class GPS_update(Resource):
+    @Action.doc(responses={200: 'Success'})
+    @Action.doc(responses={401: 'Unauthorized'})
+    @Action.doc(responses={500: 'Internal Error'})
+    def post(self):
+        token = request.headers.get('Authorization')
+        if not token:
+            return {'result': 'fail', 'msg': '토큰이 없습니다.'}
+        try:
+            # Decode the token to verify it
+            decoded_token = jwt.decode(token, "secret", algorithms=['HS256'])
+            user_id = decoded_token['id']
+        except jwt.ExpiredSignatureError:
+            return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401
+        except jwt.InvalidTokenError:
+            return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401
+
+        db = DB()
+
+        data = request.get_json()
+        if len(data["trip_id"]) !=64:
+            return {500 :"ERROR! INVALID TRIP_ID!"}
+            
+        if len(data["trip_log"]["timestamp"]) == 0:
+            return {500 :"ERROR! 'trip_log' is empty!"}
+        
+        time_stamp_len = len(data["trip_log"]["timestamp"])
+        latitude_len = len(data["trip_log"]["latitude"])
+        longitude_len = len(data["trip_log"]["longitude"])
+       
+        if time_stamp_len != latitude_len or latitude_len != longitude_len:
+            return {
+                    500: f"ERROR! Mismatching length of data in trip_log! \n timestamp : {time_stamp_len} \n latitude : {latitude_len} \n longitude : {longitude_len}"
+                   }
+        with pd.option_context('display.max_rows', None, 'display.max_columns', None):
+            print(data)
+        df = pd.DataFrame(data["trip_log"])
+        df["user_id"] = data["user_id"]
+        df["trip_id"] = data["trip_id"]
+
+        columns = df.columns
+        data_csv_block = df.to_csv(header=False, index=False)
+        print(f"-------------------------------------")
+        print(f"recieved trip_id : {df['trip_id'][0]}")
+        print(f"recieved time    : {datetime.datetime.now()}")
+        # GPS 데이터베이스에 삽입
+        db.insert_gps_data(data_csv_block, columns)
+
+        acc_df = pd.DataFrame({
+            'trip_id': data["trip_id"],
+            'user_id': data["user_id"],
+            'accel_x': data["accel"]["x"],
+            'accel_y': data["accel"]["y"],
+            'accel_z': data["accel"]["z"],
+            'timestamp': data["accel"]["timestamp"]
+        })
+        accel_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'accel_x', 'accel_y', 'accel_z']
+        accel_csv_data = acc_df[accel_columns_ordered].to_csv(header=False, index=False)
+        db.insert_accel_data(accel_csv_data, accel_columns_ordered)
+
+        gyro_df = pd.DataFrame({
+            'trip_id': data["trip_id"],
+            'user_id': data["user_id"],
+            'gyro_x': data["gyro"]["x"],
+            'gyro_y': data["gyro"]["y"],
+            'gyro_z': data["gyro"]["z"],
+            'timestamp': data["gyro"]["timestamp"]
+        })
+        gyro_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'gyro_x', 'gyro_y', 'gyro_z']
+        gyro_csv_data = gyro_df[gyro_columns_ordered].to_csv(header=False, index=False)
+        db.insert_gyro_data(gyro_csv_data, gyro_columns_ordered)
+
+        motion_df = pd.DataFrame({
+            'trip_id': data["trip_id"],
+            'user_id': data["user_id"],
+            'motion_pitch': data["motion"]["pitch"],
+            'motion_roll': data["motion"]["roll"],
+            'motion_yaw': data["motion"]["yaw"],
+            'timestamp': data["motion"]["timestamp"]
+        })
+        motion_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'motion_pitch', 'motion_roll', 'motion_yaw']
+        motion_csv_data = motion_df[motion_columns_ordered].to_csv(header=False, index=False)
+        db.insert_motion_data(motion_csv_data, motion_columns_ordered)
+
+        azimuth_df = pd.DataFrame({
+            'trip_id': data["trip_id"],
+            'user_id': data["user_id"],
+            'timestamp': data["azimuth"]["timestamp"],
+            'azimuth_heading': data["azimuth"]["heading"]
+        })
+        azimuth_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'azimuth_heading']
+        azimuth_csv_data = azimuth_df[azimuth_columns_ordered].to_csv(header=False, index=False)
+        db.insert_azimuth_data(azimuth_csv_data, azimuth_columns_ordered)
+
+        return {'result': f'success'}
+
+
+@Action.route('/trip_and_score_update')
+class TRIP_insert(Resource):
+    @Action.expect(trip_log_model)
+    @Action.doc(responses={200: 'Success'})
+    @Action.doc(responses={401: 'Unauthorized'})
+    @Action.doc(responses={500: 'Internal Error'})
+    def post(self):
+        token = request.headers.get('Authorization')
+
+        # Check if token is provided
+        if not token:
+            return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401
+
+        try:
+            # Decode the token to verify it
+            decoded_token = jwt.decode(token, "secret", algorithms=['HS256'])
+            user_id = decoded_token['id']
+        except jwt.ExpiredSignatureError:
+            return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401
+        except jwt.InvalidTokenError:
+            return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401
+
+        db = DB()
+        data = request.get_json()
+        if len(data["trip_id"]) != 64:
+            return {"result" : "ERROR! INVALID TRIP_ID!"}, 500
+
+        trip_id = data["trip_id"]
+        trip_distance_m = data["total_distance_m"]
+        trip_time_s = data["total_time_s"]
+        abrupt_start_count = data["abrupt_start_count"]
+        abrupt_stop_count = data["abrupt_stop_count"]
+        abrupt_acceleration_count = data["abrupt_acceleration_count"]
+        abrupt_deceleration_count = data["abrupt_deceleration_count"]
+        helmet_on = helmet_on = 1 if data["helmet_on"] == "true" else 0
+        final_score = data["final_score"]
+
+        if (helmet_on != 1) and (helmet_on != 0):
+            return {"result" : f"ERROR! INVALID 'helmet_on'! \n helmet_on : {helmet_on}"}, 500
+        db.insert_trip_data(
+            user_id,
+            trip_id,
+            trip_distance_m,
+            trip_time_s,
+            abrupt_start_count,
+            abrupt_stop_count,
+            abrupt_acceleration_count,
+            abrupt_deceleration_count,
+            helmet_on,
+            final_score
+        )
+        return {'result': f'success'}
+
+@Action.route('/get_history')
+class Get_history(Resource):
+    @Action.expect(history_request_model)
+    @Action.doc(responses={401: 'Unauthorized'})
+    @Action.doc(responses={500: 'Internal Error'})
+    def post(self):
+        token = request.headers.get('Authorization')
+
+        # Check if token is provided
+        if not token:
+            return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401
+       
+        try:
+            # Decode the token to verify it
+            decoded_token = jwt.decode(token, "secret", algorithms=['HS256'])
+            user_id = decoded_token['id']
+        except jwt.ExpiredSignatureError:
+            return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401
+        except jwt.InvalidTokenError:
+            return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401
+
+        # Interact with the DB to get user history
+
+        data = request.get_json()
+        user_id = data["user_id"]
+        try:
+            db = DB()
+            result, status_code = db.get_history(user_name=user_id)
+            return {'result': 'success', 'data': result}, status_code
+        except Exception as e:
+            print(str(e))
+            return {'result': 'fail', 'msg': str(e)}, 500
+
+
+@Action.route('/get_history_main')
+class Get_history_main(Resource):
+    @Action.expect(history_request_model)
+    @Action.doc(responses={401: 'Unauthorized'})
+    @Action.doc(responses={500: 'Internal Error'})
+    def post(self):
+        token = request.headers.get('Authorization')
+        if not token:
+            return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401
+
+        try:
+            decoded_token = jwt.decode(token, "secret", algorithms=['HS256'])
+            user_id = decoded_token['id']
+        except jwt.ExpiredSignatureError:
+            return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401
+        except jwt.InvalidTokenError:
+            return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401
+
+        data = request.get_json()
+        user_id = data["user_id"]
+
+        try:
+            db = DB()
+            result, status_code = db.get_history_main(user_name=user_id)
+
+            if not result:  # `result`가 비어있는 경우
+                return {'result': 'success', 'data': []}, 200
+            return {'result': 'success', 'data': result}, status_code
+        except Exception as e:
+            print(str(e))
+            return {'result': 'fail', 'msg': str(e)}, 500
+
+
+@Action.route('/get_history_by_period')
+class GetHistoryByPeriod(Resource):
+    @Action.expect(history_request_model)  # 요청 모델 정의 필요
+    @Action.doc(responses={401: 'Unauthorized'})
+    @Action.doc(responses={500: 'Internal Error'})
+    def post(self):
+        token = request.headers.get('Authorization')
+
+        if not token:
+            return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401
+
+        try:
+            decoded_token = jwt.decode(token, "secret", algorithms=['HS256'])
+            user_id = decoded_token['id']
+        except jwt.ExpiredSignatureError:
+            return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401
+        except jwt.InvalidTokenError:
+            return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401
+
+        # 요청 데이터 가져오기
+        data = request.get_json()
+        start_date = data.get("start_date")
+        end_date = data.get("end_date")
+
+        if not start_date or not end_date:
+            return {'result': 'fail', 'msg': 'start_date와 end_date가 필요합니다.'}, 400
+
+        try:
+            db = DB()
+            result, status_code = db.get_history_by_period(user_name=user_id, start_date=start_date, end_date=end_date)
+            return {'result': 'success', 'data': result}, status_code
+        except Exception as e:
+            print(str(e))
+            return {'result': 'fail', 'msg': str(e)}, 500
+
+
+@Action.route('/ping')
+class Ping(Resource):
+    def post(self):
+        return {"result" : 'success', "msg" : "pong!"}, 200
+            
+
 
auth.py (added)
+++ auth.py
@@ -0,0 +1,178 @@
+from flask import request,jsonify
+from flask_restx import Resource, Namespace, fields
+from database.database import DB
+import datetime
+import jwt
+
+
+users = {}
+
+Auth = Namespace(
+    name="Auth",
+    description="사용자 인증을 위한 API",
+)
+
+
+user_fields = Auth.model('User', {  # Model 객체 생성
+    'id': fields.String(description='a User Name', required=True, example="id")
+})
+
+
+user_fields_auth = Auth.inherit('User Auth', user_fields, {
+    'password': fields.String(description='Password', required=True)
+
+})
+
+
+get_phone_number = Auth.inherit('get a phone number of an user', {
+    'id' : fields.String(description="user id", required=True)
+})
+
+
+get_email = Auth.inherit('get an email of an user', {
+    'id' : fields.String(description="user id", required=True)
+})
+
+user_fields_register = Auth.inherit('User reigster', user_fields, {
+    'password': fields.String(description='Password', required=True),'email': fields.String(description='email', required=True),'phone': fields.String(description='phone', required=True)
+
+})
+
+
+
+@Auth.route('/id')
+class AuthCheck(Resource):
+    @Auth.doc(responses={200: 'Success'})
+    @Auth.doc(responses={500: 'Register Failed'})
+    def post(self):
+        db=DB()
+        id = request.json['id']
+        value=db.db_check_id(id)
+        if value != None:
+            return {
+                "message": "중복 아이디가 있습니다"
+            }, 500
+        else:
+            return {
+                'message': '사용가능한 아이디입니다'  # str으로 반환하여 return
+            }, 200
+
+
+@Auth.route('/register')
+class AuthRegister(Resource):
+    @Auth.expect(user_fields_register)
+    @Auth.doc(responses={200: 'Success'})
+    @Auth.doc(responses={500: 'Register Failed'})
+    def post(self):
+        user_manager = DB()
+        # Extract data from the request
+        data = request.json
+        id_ = data['id']
+        password = data['password']
+        user_email = data['email']
+        # sex = data['user_sex']
+        phone = data['phone']
+
+        # Prepare data for registration
+        user_data = {
+            'username': id_,
+            'password': password,
+            'email': user_email,
+            # 'sex': sex,
+            'phone': phone
+        }
+
+        # Call the register_user method from the UserManager instance
+        result, status_code = user_manager.register_user(user_data)
+
+        # Return the appropriate response based on the result from UserManager
+        if status_code == 200:
+            return result, 200
+        else:
+            return result, 500
+
+@Auth.route('/retrive_phone_number')
+class AuthRegister(Resource):
+    @Auth.expect(get_phone_number)
+    @Auth.doc(responses={200: 'Success'})
+    @Auth.doc(responses={500: 'Register Failed'})
+    def post(self):
+        user_manager = DB()
+        data = request.json
+        id_ = data['id']
+        query_input = {
+            "username" : id_
+        }
+        result, status_code = user_manager.get_phone_number(query_input)
+
+        if status_code == 200:
+            return result, 200
+        else:
+            return result, 500
+
+
+@Auth.route('/retrive_email')
+class AuthRegister(Resource):
+    @Auth.expect(get_email)
+    @Auth.doc(responses={200: 'Success'})
+    @Auth.doc(responses={500: 'Register Failed'})
+    def post(self):
+        user_manager = DB()
+        data = request.json
+        id_ = data['id']
+        query_input = {
+            "username" : id_
+        }
+        result, status_code = user_manager.get_email(query_input)
+
+        if status_code == 200:
+            return result, 200
+        else:
+            return result, 500
+
+
+
+@Auth.route('/login')
+class AuthLogin(Resource):
+    @Auth.expect(user_fields_auth)
+    @Auth.doc(responses={200: 'Login Successful'})
+    @Auth.doc(responses={401: 'Unauthorized'})
+    @Auth.doc(responses={500: 'Login Failed'})
+    def post(self):
+        user_manager = DB()
+        # Extract data from the request
+        data = request.json
+        id_ = data['id']
+        password = data['password']
+
+        # Prepare data for authentication
+        user_data = {
+            'username': id_,
+            'password': password
+        }
+
+        # Call the login_user method from the UserManager instance
+        result, status_code = user_manager.login_user(user_data)
+
+        if result['status'] == 'success':
+            payload = {
+                'id': id_,
+                'exp': datetime.datetime.utcnow() + datetime.timedelta(days=14)
+            }
+            token = jwt.encode(payload, "secret", algorithm='HS256')
+            return {'result': 'success', 'token': token}, 200
+        else :
+            return {'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'}, 401
+
+
+@Auth.route('/withdraw')
+class AuthWithdraw(Resource):
+    def post(self):
+         db=DB()
+         id = request.json['token']
+         payload = jwt.decode(id, "secret", algorithms=['HS256'])
+         db.db_delete_id(payload['id'])
+         return {'secession':'success'}, 200
+
+
+
 
database/database.py (added)
+++ database/database.py
@@ -0,0 +1,544 @@
+import psycopg2 # driver 임포트
+import json
+import bcrypt
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.backends import default_backend
+import re
+import os
+from io import StringIO
+from datetime import datetime, timedelta
+
+config_file_path = "database/db_config.json"
+
+class DB():
+    def __init__(self):
+
+        # Load the database configuration from the JSON file
+        self.db_config = self.load_db_config(config_file_path)
+
+        # Initialize database connection
+        self.conn = psycopg2.connect(
+            host=self.db_config['host'],
+            dbname=self.db_config['dbname'],
+            user=self.db_config['user'],
+            password=self.db_config['password'],
+            port=self.db_config['port'],
+            # options=self.db_config['options']
+        )
+        self.schema = self.db_config["schema"]
+
+        self.conn.autocommit=True
+        self.cur = self.conn.cursor()
+        # yeah, that double quotation is absolutely needed (to distinguish capital letters)
+        self.cur.execute("SET search_path TO " + f'"{self.schema}"')
+        with open("database/keys/encryption_key2025-04-03_14:33:09", "rb") as f:
+            self.encryption_key = f.read()
+
+    def load_db_config(self, config_file_path):
+        """
+        Loads database configuration from a JSON file.
+        """
+        with open(config_file_path, 'r') as config_file:
+            return json.load(config_file)
+
+    def encrypt_aes(self, plain_text):
+        iv = os.urandom(16)  # AES block size is 16 bytes
+        cipher = Cipher(algorithms.AES(self.encryption_key), modes.CBC(iv), backend=default_backend())
+        encryptor = cipher.encryptor()
+
+        # Pad the plaintext to be a multiple of 16 bytes
+        padder = padding.PKCS7(algorithms.AES.block_size).padder()
+        padded_data = padder.update(plain_text.encode('utf-8')) + padder.finalize()
+
+        encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
+        return encrypted_data, iv
+
+    def decrypt_aes(self, encrypted_data, iv):
+        cipher = Cipher(algorithms.AES(self.encryption_key), modes.CBC(iv), backend=default_backend())
+        decryptor = cipher.decryptor()
+
+        decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
+
+        # Remove padding after decryption
+        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
+        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
+
+        return unpadded_data.decode('utf-8')
+
+    def cleanse_and_validate_input(self, data):
+        """
+        Cleanses input by removing leading/trailing spaces and validates the data.
+        Returns cleansed data and an error message if validation fails.
+        """
+        username = data.get('username', '').strip()
+        password = data.get('password', '').strip()
+        email = data.get('email', '').strip()
+        phone = data.get('phone', '').strip()
+        phone = phone.replace("-","")
+        sex = data.get('sex', '').strip()
+
+        # Validate username
+        if not username:
+            return None, "Username is required."
+        if len(username) > 26:
+            return None, "Username must not exceed 26 characters."
+
+        # Validate password
+        if not password:
+            return None, "Password is required."
+        if len(password) < 8:
+            return None, "Password must be at least 8 characters long."
+
+        # Validate email format
+        if not email or not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email):
+            return None, "Invalid email address."
+
+        # Validate phone number format
+        if not re.fullmatch(r'010\d{8}', phone):
+            return None, "Phone number must be in the format 010XXXXXXXX where X are digits."
+
+        # Validate sex input
+        # if not sex:
+        #    return None, "Sex is required."
+        # if sex not in ['Male', 'Female', 'Non-binary', 'Other']:
+        #    return None, "Invalid value for sex."
+        sex = "WHATEVER"
+        return {
+            'username': username,
+            'password': password,
+            'email': email,
+            'phone': phone,
+            'sex': sex
+        }, None
+
+    def register_user(self, data):
+        data, error = self.cleanse_and_validate_input(data)
+        if error:
+            return {'status': 'error', 'message': error}, 400
+
+        username = data['username']
+        password = data['password']
+        email = data['email']
+        phone = data['phone']
+        sex = data['sex']
+
+        # Hash the password with bcrypt, which automatically handles the salt
+        hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
+
+        # Encrypt email, phone, and sex with AES
+        encrypted_email, email_iv = self.encrypt_aes(email)
+        encrypted_phone, phone_iv = self.encrypt_aes(phone)
+        encrypted_sex, sex_iv = self.encrypt_aes(sex)
+
+        # Insert the user into the database
+        try:
+            self.cur.execute(f"""
+                INSERT INTO users (username, user_pw, user_email, email_iv, user_phone, phone_iv, user_time_stamp)
+                VALUES (%s, %s, %s, %s, %s, %s, %s)
+            """, (
+                username,
+                psycopg2.Binary(hashed_pw),
+                psycopg2.Binary(encrypted_email),
+                psycopg2.Binary(email_iv),
+                psycopg2.Binary(encrypted_phone),
+                psycopg2.Binary(phone_iv),
+                datetime.now()  # Correct way to insert current timestamp with timezone
+            )
+                             )
+            self.conn.commit()
+            return {'status': 'success', 'message': f'user {username} registered successfully'}, 200
+        except psycopg2.Error as e:
+            self.conn.rollback()
+            return {'status': 'error', 'message': str(e)}, 400
+
+    def login_user(self, data):
+        username = data.get('username', '').strip()
+        password = data.get('password', '').strip()
+
+        # Validate input
+        if not username or not password:
+            return {'status': 'error', 'message': 'Username and password are required.'}, 400
+
+        # Retrieve the user's hashed password from the database
+        self.cur.execute("SELECT user_pw FROM users WHERE username = %s", (username,))
+        user = self.cur.fetchone()
+
+        if user is None:
+            return {'status': 'error', 'message': 'Invalid username or password'}, 401
+
+        hashed_pw = bytes(user[0])  # Convert the retrieved hashed password to bytes
+
+        # Check if the provided password matches the stored hashed password
+        if bcrypt.checkpw(password.encode('utf-8'), hashed_pw):
+            return {'status': 'success', 'message': 'Logged in successfully'}, 200
+        else:
+            return {'status': 'error', 'message': 'Invalid username or password'}, 401
+
+    def get_phone_number(self, data):
+        username = data.get('username', '').strip()
+
+        if not username:
+            return {'status': 'error', 'message': 'Username is required.'}, 400
+
+        # Retrieve the encrypted phone number and IV from the database
+        self.cur.execute("SELECT user_phone, phone_iv FROM users WHERE username = %s", (username,))
+        user = self.cur.fetchone()
+
+        if user is None:
+            return {'status': 'error', 'message': 'User not found'}, 404
+
+        encrypted_phone, phone_iv = user
+
+        # Decrypt the phone number
+        decrypted_phone = self.decrypt_aes(encrypted_phone, phone_iv)
+
+        return {'status': 'success', 'phone_number': decrypted_phone}, 200
+
+
+    def get_email(self, data):
+        username = data.get('username', '').strip()
+
+        if not username:
+            return {'status': 'error', 'message': 'Username is required.'}, 400
+
+        # Retrieve the encrypted phone number and IV from the database
+        self.cur.execute("SELECT user_email, email_iv FROM users WHERE username = %s", (username,))
+        user = self.cur.fetchone()
+
+        if user is None:
+            return {'status': 'error', 'message': 'User not found'}, 404
+
+        encrypted_phone, phone_iv = user
+
+        # Decrypt the phone number
+        decrypted_phone = self.decrypt_aes(encrypted_phone, phone_iv)
+
+        return {'status': 'success', 'phone_number': decrypted_phone}, 200
+
+    def insert_gps_data(self, csv_block, columns):
+        cur = self.conn.cursor()
+        data = StringIO(csv_block)
+
+        # using COPY instead of INSERT to do even less operation per data.
+        cur.copy_from(data, 'gps_data', sep=',', columns=columns)
+        self.conn.commit()
+        cur.close()
+        return True
+
+    def insert_accel_data(self, csv_block, columns):
+        cur = self.conn.cursor()
+        try:
+            data = StringIO(csv_block) # Create StringIO internally
+            cur.copy_from(data, f'accel_data', sep=',', columns=columns)
+            self.conn.commit()
+            success = True
+        except Exception as e:
+            self.conn.rollback()
+            print(f"DB Error inserting Accel: {e}")
+            success = False
+        finally:
+             if cur:
+                  cur.close()
+        return success
+
+    def insert_gyro_data(self, csv_block, columns):
+        cur = self.conn.cursor()
+        try:
+            data = StringIO(csv_block) # Create StringIO internally
+            cur.copy_from(data, f'gyro_data', sep=',', columns=columns)
+            self.conn.commit()
+            success = True
+        except Exception as e:
+            self.conn.rollback()
+            print(f"DB Error inserting Gyro: {e}")
+            success = False
+        finally:
+             if cur:
+                  cur.close()
+        return success
+
+    def insert_motion_data(self, csv_block, columns):
+        cur = self.conn.cursor()
+        try:
+            data = StringIO(csv_block) # Create StringIO internally
+            cur.copy_from(data, f'motion_data', sep=',', columns=columns)
+            self.conn.commit()
+            success = True
+        except Exception as e:
+            self.conn.rollback()
+            print(f"DB Error inserting Motion: {e}")
+            success = False
+        finally:
+             if cur:
+                  cur.close()
+        return success
+
+    def insert_azimuth_data(self, csv_block, columns):
+        cur = self.conn.cursor()
+        try:
+            data = StringIO(csv_block) # Create StringIO internally
+            cur.copy_from(data, f'azimuth_data', sep=',', columns=columns)
+            self.conn.commit()
+            success = True
+        except Exception as e:
+            self.conn.rollback()
+            print(f"DB Error inserting Azimuth: {e}")
+            success = False
+        finally:
+             if cur:
+                  cur.close()
+        return success
+
+    def insert_trip_data(
+            self,
+            username,
+            trip_id,
+            total_distance_m,
+            total_time_s,
+            abrupt_start_count,
+            abrupt_stop_count,
+            abrupt_acceleration_count,
+            abrupt_deceleration_count,
+            helmet_on,
+            final_score
+    ):
+
+        self.cur.execute(f"""
+            INSERT INTO trip_log (user_id, trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, abrupt_stop_count,
+             abrupt_acceleration_count, abrupt_deceleration_count, helmet_on, final_score)
+            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+        """, (
+            username,
+            trip_id,
+            datetime.now(),
+            total_distance_m,
+            total_time_s,
+            abrupt_start_count,
+            abrupt_stop_count,
+            abrupt_acceleration_count,
+            abrupt_deceleration_count,
+            helmet_on,
+            final_score
+            )
+        )
+
+    def db_delete_id(self,user_id) :
+        cur = self.conn.cursor()
+        cur.execute(f'''
+        delete
+        from "{self.schema}".user_id ui
+        where user_id  = '{user_id}'
+        ''')
+        cur.close()
+
+    def get_history(self, user_name):
+        """
+        Retrieves all trip logs for the specified user within the last month and returns them in JSON format.
+            [
+              {
+                "trip_id": "trip_001",
+                "timestamp": "2024-09-01 12:45:00",
+                "total_distance_m": 1000.5,
+                "total_time_s": 600,
+                "abrupt_start_count": 3,
+                "abrupt_stop_count": 2,
+                "abrupt_acceleration_count": 1,
+                "abrupt_deceleration_count": 1,
+                "helmet_on": true,
+                "final_score": 85.5
+              },
+              {
+                "trip_id": "trip_002",
+                "timestamp": "2024-09-02 14:30:00",
+                "total_distance_m": 1500.0,
+                "total_time_s": 720,
+                "abrupt_start_count": 2,
+                "abrupt_stop_count": 3,
+                "abrupt_acceleration_count": 1,
+                "abrupt_deceleration_count": 2,
+                "helmet_on": false,
+                "final_score": 90.0
+              }
+            ]
+        """
+        try:
+            # Execute the query to retrieve logs within the last month
+            self.cur.execute("""
+                SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, 
+                       abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, 
+                       helmet_on, final_score
+                FROM trip_log
+                WHERE user_id = %s
+                AND timestamp >= NOW() - INTERVAL '1 month'
+            """, (user_name,))
+
+            # Fetch all results
+            rows = self.cur.fetchall()
+
+            # Format the results into a list of dictionaries
+            result = []
+            for row in rows:
+                trip_log = {
+                    "trip_id": row[0],
+                    "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"),  # Format timestamp
+                    "total_distance_m": row[2],
+                    "total_time_s": row[3],
+                    "abrupt_start_count": row[4],
+                    "abrupt_stop_count": row[5],
+                    "abrupt_acceleration_count": row[6],
+                    "abrupt_deceleration_count": row[7],
+                    "helmet_on": bool(row[8]),  # Convert INT to Boolean
+                    "final_score": row[9]
+                }
+                result.append(trip_log)
+
+            # Convert the result list to JSON format
+            return json.dumps(result), 200
+
+        except psycopg2.Error as e:
+            self.conn.rollback()
+            return {'status': 'error', 'message': str(e)}, 500
+
+    def get_history_main(self, user_name):
+        try:
+            # 최신 기록 1건 조회
+            self.cur.execute("""
+                SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, 
+                       abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, 
+                       helmet_on, final_score
+                FROM trip_log
+                WHERE user_id = %s
+                ORDER BY timestamp DESC
+                LIMIT 1
+            """, (user_name,))
+            row = self.cur.fetchone()
+
+            if not row:
+                return json.dumps({"message": "No records found", "data": []}), 200
+
+            latest_record = {
+                "trip_id": row[0],
+                "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"),
+                "total_distance_m": row[2],
+                "total_time_s": row[3],
+                "abrupt_start_count": row[4],
+                "abrupt_stop_count": row[5],
+                "abrupt_acceleration_count": row[6],
+                "abrupt_deceleration_count": row[7],
+                "helmet_on": bool(row[8]),
+                "final_score": row[9]
+            }
+
+            # 전체 기록 수 조회
+            self.cur.execute("SELECT COUNT(*) FROM trip_log WHERE user_id = %s", (user_name,))
+            total_count = self.cur.fetchone()[0]
+
+            # final_score가 95점 이상인 기록 수 조회
+            self.cur.execute("SELECT COUNT(*) FROM trip_log WHERE user_id = %s AND final_score >= 95", (user_name,))
+            count_above_95 = self.cur.fetchone()[0]
+
+            # 평균 final_score 조회
+            self.cur.execute("SELECT AVG(final_score) FROM trip_log WHERE user_id = %s", (user_name,))
+            avg_final_score = self.cur.fetchone()[0]
+            avg_final_score = round(avg_final_score, 2)
+
+            # 응답 데이터 구성
+            response_data = {
+                "latest_record": latest_record,
+                "total_count": total_count,
+                "count_above_95": count_above_95,
+                "average_final_score": avg_final_score
+            }
+
+            return json.dumps(response_data), 200
+
+        except psycopg2.Error as e:
+            self.conn.rollback()
+            return {'status': 'error', 'message': str(e)}, 500
+
+    def get_history_by_period(self, user_name, start_date, end_date):
+        try:
+            # Execute the query to retrieve logs within the specified period
+            self.cur.execute("""
+                SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, 
+                       abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, 
+                       helmet_on, final_score
+                FROM trip_log
+                WHERE user_id = %s
+                AND timestamp BETWEEN %s AND %s
+            """, (user_name, start_date, end_date))
+
+            # Fetch all results
+            rows = self.cur.fetchall()
+
+            # Format the results into a list of dictionaries
+            result = []
+            final_scores = []
+            daily_scores = {}
+
+            for row in rows:
+                trip_log = {
+                    "trip_id": row[0],
+                    "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"),  # Format timestamp
+                    "total_distance_m": row[2],
+                    "total_time_s": row[3],
+                    "abrupt_start_count": row[4],
+                    "abrupt_stop_count": row[5],
+                    "abrupt_acceleration_count": row[6],
+                    "abrupt_deceleration_count": row[7],
+                    "helmet_on": bool(row[8]),  # Convert INT to Boolean
+                    "final_score": row[9]
+                }
+                result.append(trip_log)
+                final_scores.append(row[9])
+
+                # 날짜별 평균 계산을 위한 데이터 정리
+                date_key = row[1].strftime("%Y-%m-%d")
+                if date_key not in daily_scores:
+                    daily_scores[date_key] = []
+                daily_scores[date_key].append(row[9])
+
+            # 전체 기간의 final_score 평균 계산
+            overall_avg_score = round(sum(final_scores) / len(final_scores), 2) if final_scores else 0
+
+            # start_date와 end_date를 datetime 객체로 변환 (이미 datetime이라면 그대로 사용)
+            if isinstance(start_date, str):
+                start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+            else:
+                start_dt = start_date
+
+            if isinstance(end_date, str):
+                end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+            else:
+                end_dt = end_date
+
+            # start_date부터 end_date까지 모든 날짜에 대해 평균 점수를 계산 (데이터가 없으면 0)
+            daily_avg_scores = {}
+            current_dt = start_dt
+            while current_dt <= end_dt:
+                day_key = current_dt.day  # 예: 1, 2, 3 등
+                full_date_key = current_dt.strftime("%Y-%m-%d")
+                scores = daily_scores.get(full_date_key, [])
+                daily_avg_scores[day_key] = round(sum(scores) / len(scores), 2) if scores else 0
+                current_dt += timedelta(days=1)
+
+            # Convert the result list to JSON format
+            response_data = {
+                "history": result,
+                "overall_avg_score": overall_avg_score,
+                "daily_avg_scores": daily_avg_scores
+            }
+
+            return json.dumps(response_data), 200
+
+        except psycopg2.Error as e:
+            self.conn.rollback()
+            return {'status': 'error', 'message': str(e)}, 500
+
+    def close_connection(self):
+        cur = self.cur
+        cur.close()
+        return True
+
+
 
database/key_gen.py (added)
+++ database/key_gen.py
@@ -0,0 +1,18 @@
+import os
+
+# NEVER be the part of server script, THIS SHOULD NEVER run with server.
+# ALSO, remember to BACKUP the key
+
+def create_and_save_key(key_file_path):
+    """
+    Generates a new AES encryption key and saves it to a file.
+    """
+    key = os.urandom(32)  # AES-256 requires a 32-byte key
+    with open(key_file_path, 'wb') as key_file:
+        key_file.write(key)
+    print(f"Encryption key created and saved to {key_file_path}")
+    return key
+
+if __name__ == "__main__":
+    from datetime import datetime
+    create_and_save_key(f"keys/encryption_key{datetime.now().strftime('%Y-%m-%d_%H:%M:%S')}")(파일 끝에 줄바꿈 문자 없음)
 
logger/__pycache__/logger.cpython-310.pyc (Binary) (added)
+++ logger/__pycache__/logger.cpython-310.pyc
Binary file is not shown
 
logger/__pycache__/logger.cpython-311.pyc (Binary) (added)
+++ logger/__pycache__/logger.cpython-311.pyc
Binary file is not shown
 
logger/logger.py (added)
+++ logger/logger.py
@@ -0,0 +1,14 @@
+from datetime import datetime
+import os
+
+if not os.path.exists("logs"):
+    os.makedirs("logs")
+    os.makedirs("logs/gunicorn")
+
+bind = "192.168.0.195:27461"
+workers = 2
+#worker_type = 'gevent'
+reload = True
+accesslog = f"./logs/gunicorn/access_{datetime.now().strftime('%Y-%m-%d_%H')}.log"
+errorlog = f"./logs/gunicorn/error_{datetime.now().strftime('%Y-%m-%d_%H')}.log"
+loglevel = "info"
Add a comment
List