commit f9713153d19e09233dafc170267c171c61abab9b Author: eld_master Date: Fri Dec 6 14:12:28 2024 +0900 첫커밋 diff --git a/fastapi/Dockerfile-fastapi b/fastapi/Dockerfile-fastapi new file mode 100644 index 0000000..5ca9e39 --- /dev/null +++ b/fastapi/Dockerfile-fastapi @@ -0,0 +1,12 @@ +FROM python:3.8 + +WORKDIR /app + +COPY ./app /app + +RUN pip install -r requirements.txt + +EXPOSE 8097 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8097"] + diff --git a/fastapi/app/__pycache__/main.cpython-38.pyc b/fastapi/app/__pycache__/main.cpython-38.pyc new file mode 100644 index 0000000..e095218 Binary files /dev/null and b/fastapi/app/__pycache__/main.cpython-38.pyc differ diff --git a/fastapi/app/common/config.py b/fastapi/app/common/config.py new file mode 100644 index 0000000..6cf4076 --- /dev/null +++ b/fastapi/app/common/config.py @@ -0,0 +1,11 @@ +# 서버 종류 +MY_SERVER = 'SERVER2' + +# JWT 키 +SECRET_KEY = 'ALLSCORE' + +# 이메일 정보 +SMTP_USERNAME = 'aofhd0003@gmail.com' +SMTP_PASSWORD = 'jueyodpzlvisrtto' +SMTP_SERVER = 'smtp.gmail.com' +SMTP_PORT = 587 \ No newline at end of file diff --git a/fastapi/app/db/__pycache__/base.cpython-38.pyc b/fastapi/app/db/__pycache__/base.cpython-38.pyc new file mode 100644 index 0000000..4973716 Binary files /dev/null and b/fastapi/app/db/__pycache__/base.cpython-38.pyc differ diff --git a/fastapi/app/db/__pycache__/crud.cpython-38.pyc b/fastapi/app/db/__pycache__/crud.cpython-38.pyc new file mode 100644 index 0000000..b9ae55f Binary files /dev/null and b/fastapi/app/db/__pycache__/crud.cpython-38.pyc differ diff --git a/fastapi/app/db/__pycache__/models.cpython-38.pyc b/fastapi/app/db/__pycache__/models.cpython-38.pyc new file mode 100644 index 0000000..a2740e9 Binary files /dev/null and b/fastapi/app/db/__pycache__/models.cpython-38.pyc differ diff --git a/fastapi/app/db/__pycache__/schemas.cpython-38.pyc b/fastapi/app/db/__pycache__/schemas.cpython-38.pyc new file mode 100644 index 0000000..a8c63db Binary files /dev/null and b/fastapi/app/db/__pycache__/schemas.cpython-38.pyc differ diff --git a/fastapi/app/db/base.py b/fastapi/app/db/base.py new file mode 100644 index 0000000..bbfc967 --- /dev/null +++ b/fastapi/app/db/base.py @@ -0,0 +1,18 @@ + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "postgresql://eldsoft:eld240510@172.31.7.121:8088/allscore" +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = None + try: + db = SessionLocal() + yield db + finally: + db.close() \ No newline at end of file diff --git a/fastapi/app/db/crud.py b/fastapi/app/db/crud.py new file mode 100644 index 0000000..6bfb3ba --- /dev/null +++ b/fastapi/app/db/crud.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text +from db import models, schemas +from datetime import datetime + +def create_card_expense(db: Session, card_expense: schemas.CardExpenseCreate): + db_card_expense = models.CardExpense(jang=card_expense.jang, yeo=card_expense.yeo, kim=card_expense.kim, choi=card_expense.choi, amount=card_expense.amount, purpose=card_expense.purpose, created_at=datetime.now()) + db.add(db_card_expense) + db.commit() + db.refresh(db_card_expense) + return db_card_expense + + +def get_card_expenses_by_date(db: Session, year: int, month: int): + query = text(""" + SELECT * FROM card_expenses + WHERE EXTRACT(YEAR FROM created_at) = :year + AND EXTRACT(MONTH FROM created_at) = :month + """) + return db.execute(query, {'year': year, 'month': month}).fetchall() \ No newline at end of file diff --git a/fastapi/app/db/crud_room_score.py b/fastapi/app/db/crud_room_score.py new file mode 100644 index 0000000..80b9bac --- /dev/null +++ b/fastapi/app/db/crud_room_score.py @@ -0,0 +1,313 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text +from db import models, schemas +from process.logger import logger +from db.base import get_db + + +# ================================================================ +# 회원관리 함수 +# ================================================================ + +# 방 생성 +def create_room(data, db): + query = text(f""" + insert into room_main(user_seq, cert_type, user_info, cert_code, cert_expired) + values({user_seq}, '{cert_type}', '{user_info}', '{cert_code}', now() + INTERVAL '5 minutes') + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + + + + + + + + + + + + + + + + + + + + + + + +# 로그인 +def do_login(user_id, user_pw, db): + query = text(f""" + select + user_seq + from manage_user + where + user_id = '{user_id}' + and user_pw = (select + encode( + digest( + '{user_pw}' || (select user_pw_solt from manage_user where user_id = '{user_id}'), 'sha256' + ), 'hex' + )) + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 마지막 로그인 시간 업데이트 +def update_last_login_dt(user_seq, db): + query = text(f""" + update manage_user + set last_login_dt = now() + where user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 아이디 찾기(닉네임, 이메일) +def find_id_by_name_email(nickname, user_email, db): + query = text(f""" + select + user_seq, + user_id + from manage_user + where nickname = '{nickname}' + and user_email = '{user_email}' + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 아이디 찾기(user_seq) +def find_id_by_user_seq(user_seq, db): + query = text(f""" + select + user_id, + user_email + from manage_user + where user_seq = {user_seq} + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 이메일 발송 3회 이상 됐는지 확인 +def select_send_email_cnt(user_email, db): + query = text(f""" + select + count(user_cert_seq) + from manage_user_cert + where user_info = '{user_email}' + and cert_expired between current_date and current_date + interval '1 day' + """) + return db.execute(query).fetchall() + + +# 이메일 발송내역 기록(하루 3회만 발송가능하게 하기 위함) +def insert_send_email_info(user_seq, cert_type, user_info, cert_code, db): + query = text(f""" + insert into manage_user_cert(user_seq, cert_type, user_info, cert_code, cert_expired) + values({user_seq}, '{cert_type}', '{user_info}', '{cert_code}', now() + INTERVAL '5 minutes') + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 비밀번호 찾기 +def find_password_by_id_email(user_id, user_email, db): + query = text(f""" + select + user_seq + from manage_user + where user_id = '{user_id}' + and user_email = '{user_email}' + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 신규 비밀번호 업데이트 +def update_new_password(user_seq, new_pw, new_solt, db): + query = text(f""" + update manage_user + set + user_pw = encode(digest(encode(digest('{new_pw}', 'sha256'), 'hex') || '{new_solt}', 'sha256'), 'hex'), + user_pw_solt = '{new_solt}' + where + user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 회원가입 최종 아이디 중복 재검사 +def is_valid_user_id_by_user_id(user_id, db): + query = text(f""" + select + user_seq + from manage_user + where user_id = '{user_id}' + """) + return db.execute(query).fetchall() + + +# 신규 유저 등록 +def insert_new_user(user_info, db): + query = text(f""" + insert into + manage_user( + user_id, + user_pw, + user_pw_solt, + nickname, + profile_img, + user_email, + department, + introduce_myself, + last_login_dt, + create_dt, + update_dt, + mandatory_terms_yn, + withdraw_yn + ) + values( + '{user_info['user_id']}', + (select encode(digest('{user_info['user_pw']}{user_info['user_pw_solt']}', 'sha256'), 'hex')), + '{user_info['user_pw_solt']}', + '{user_info['nickname']}', + 'images/user/temp_dir/profile_img/basic_{user_info['user_id']}.png', + '{user_info['user_email']}', + '{user_info.get('department', '')}', + '{user_info.get('introduce_myself', '')}', + now(), + now(), + now(), + '{user_info.get('mandatory_terms_yn', '')}', + 'N' + ) + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# user_seq로 내정보 가져오기 +def get_my_info_by_user_seq(user_seq, db): + query = text(f""" + select + nickname, + user_email, + department, + profile_img, + introduce_myself + from manage_user + where user_seq = {user_seq} + """) + return db.execute(query).fetchall() + + +# 현재 비밀번호 일치 확인 +def check_current_user_pw(user_seq, user_pw, db): + query = text(f""" + select + count(user_seq) + from manage_user + where user_seq = {user_seq} + and user_pw = (select + encode( + digest( + '{user_pw}' || (select user_pw_solt from manage_user where user_seq = '{user_seq}'), 'sha256' + ), 'hex' + )) + """) + return db.execute(query).fetchall() + + +# 신규 비밀번호 업데이트 +def update_user_info(user_info, db): + if user_info['user_pw_change_yn'] == 'Y': + query = text(f""" + update manage_user + set + user_pw = encode(digest(encode(digest('{user_info['new_user_pw']}', 'sha256'), 'hex') || '{user_info['user_pw_solt']}', 'sha256'), 'hex'), + user_pw_solt = '{user_info['user_pw_solt']}' + nickname = '{user_info['nickname']}', + user_email = '{user_info['user_email']}', + department = '{user_info['department']}', + profile_img = '{user_info['profile_img']}', + introduce_myself = '{user_info['introduce_myself']}' + where + user_seq = {user_info['user_seq']} + and withdraw_yn = 'N' + """) + else: + query = text(f""" + update manage_user + set + nickname = '{user_info['nickname']}', + user_email = '{user_info['user_email']}', + department = '{user_info['department']}', + profile_img = '{user_info['profile_img']}', + introduce_myself = '{user_info['introduce_myself']}' + where + user_seq = {user_info['user_seq']} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 회원 탈퇴 처리 +def user_withdraw(user_seq, db): + query = text(f""" + delete from manage_user + where + user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + diff --git a/fastapi/app/db/crud_user.py b/fastapi/app/db/crud_user.py new file mode 100644 index 0000000..2b620f3 --- /dev/null +++ b/fastapi/app/db/crud_user.py @@ -0,0 +1,276 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text +from db import models, schemas +from process.logger import logger +from db.base import get_db + + +# ================================================================ +# 회원관리 함수 +# ================================================================ + +# 로그인 +def do_login(user_id, user_pw, db): + query = text(f""" + select + user_seq + from manage_user + where + user_id = '{user_id}' + and user_pw = (select + encode( + digest( + '{user_pw}' || (select user_pw_solt from manage_user where user_id = '{user_id}'), 'sha256' + ), 'hex' + )) + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 마지막 로그인 시간 업데이트 +def update_last_login_dt(user_seq, db): + query = text(f""" + update manage_user + set last_login_dt = now() + where user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 아이디 찾기(닉네임, 이메일) +def find_id_by_name_email(nickname, user_email, db): + query = text(f""" + select + user_seq, + user_id + from manage_user + where nickname = '{nickname}' + and user_email = '{user_email}' + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 아이디 찾기(user_seq) +def find_id_by_user_seq(user_seq, db): + query = text(f""" + select + user_id, + user_email + from manage_user + where user_seq = {user_seq} + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 이메일 발송 3회 이상 됐는지 확인 +def select_send_email_cnt(user_email, db): + query = text(f""" + select + count(user_cert_seq) + from manage_user_cert + where user_info = '{user_email}' + and cert_expired between current_date and current_date + interval '1 day' + """) + return db.execute(query).fetchall() + + +# 이메일 발송내역 기록(하루 3회만 발송가능하게 하기 위함) +def insert_send_email_info(user_seq, cert_type, user_info, cert_code, db): + query = text(f""" + insert into manage_user_cert(user_seq, cert_type, user_info, cert_code, cert_expired) + values({user_seq}, '{cert_type}', '{user_info}', '{cert_code}', now() + INTERVAL '5 minutes') + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 비밀번호 찾기 +def find_password_by_id_email(user_id, user_email, db): + query = text(f""" + select + user_seq + from manage_user + where user_id = '{user_id}' + and user_email = '{user_email}' + and withdraw_yn = 'N' + """) + return db.execute(query).fetchall() + + +# 신규 비밀번호 업데이트 +def update_new_password(user_seq, new_pw, new_solt, db): + query = text(f""" + update manage_user + set + user_pw = encode(digest(encode(digest('{new_pw}', 'sha256'), 'hex') || '{new_solt}', 'sha256'), 'hex'), + user_pw_solt = '{new_solt}' + where + user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 회원가입 최종 아이디 중복 재검사 +def is_valid_user_id_by_user_id(user_id, db): + query = text(f""" + select + user_seq + from manage_user + where user_id = '{user_id}' + """) + return db.execute(query).fetchall() + + +# 신규 유저 등록 +def insert_new_user(user_info, db): + query = text(f""" + insert into + manage_user( + user_id, + user_pw, + user_pw_solt, + nickname, + profile_img, + user_email, + department, + introduce_myself, + last_login_dt, + create_dt, + update_dt, + mandatory_terms_yn, + withdraw_yn + ) + values( + '{user_info['user_id']}', + (select encode(digest('{user_info['user_pw']}{user_info['user_pw_solt']}', 'sha256'), 'hex')), + '{user_info['user_pw_solt']}', + '{user_info['nickname']}', + 'images/user/temp_dir/profile_img/basic_{user_info['user_id']}.png', + '{user_info['user_email']}', + '{user_info.get('department', '')}', + '{user_info.get('introduce_myself', '')}', + now(), + now(), + now(), + '{user_info.get('mandatory_terms_yn', '')}', + 'N' + ) + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# user_seq로 내정보 가져오기 +def get_my_info_by_user_seq(user_seq, db): + query = text(f""" + select + nickname, + user_email, + department, + profile_img, + introduce_myself + from manage_user + where user_seq = {user_seq} + """) + return db.execute(query).fetchall() + + +# 현재 비밀번호 일치 확인 +def check_current_user_pw(user_seq, user_pw, db): + query = text(f""" + select + count(user_seq) + from manage_user + where user_seq = {user_seq} + and user_pw = (select + encode( + digest( + '{user_pw}' || (select user_pw_solt from manage_user where user_seq = '{user_seq}'), 'sha256' + ), 'hex' + )) + """) + return db.execute(query).fetchall() + + +# 신규 비밀번호 업데이트 +def update_user_info(user_info, db): + if user_info['user_pw_change_yn'] == 'Y': + query = text(f""" + update manage_user + set + user_pw = encode(digest(encode(digest('{user_info['new_user_pw']}', 'sha256'), 'hex') || '{user_info['user_pw_solt']}', 'sha256'), 'hex'), + user_pw_solt = '{user_info['user_pw_solt']}' + nickname = '{user_info['nickname']}', + user_email = '{user_info['user_email']}', + department = '{user_info['department']}', + profile_img = '{user_info['profile_img']}', + introduce_myself = '{user_info['introduce_myself']}' + where + user_seq = {user_info['user_seq']} + and withdraw_yn = 'N' + """) + else: + query = text(f""" + update manage_user + set + nickname = '{user_info['nickname']}', + user_email = '{user_info['user_email']}', + department = '{user_info['department']}', + profile_img = '{user_info['profile_img']}', + introduce_myself = '{user_info['introduce_myself']}' + where + user_seq = {user_info['user_seq']} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + + +# 회원 탈퇴 처리 +def user_withdraw(user_seq, db): + query = text(f""" + delete from manage_user + where + user_seq = {user_seq} + and withdraw_yn = 'N' + """) + try: + db.execute(query) + db.commit() + return True + except Exception as e: + logger.error(f"sql error: {e}") + return False + diff --git a/fastapi/app/db/models.py b/fastapi/app/db/models.py new file mode 100644 index 0000000..ddf6c5b --- /dev/null +++ b/fastapi/app/db/models.py @@ -0,0 +1,63 @@ +from sqlalchemy import Column, Integer, String, CHAR, TIMESTAMP, VARCHAR, FLOAT, JSON, Numeric, DateTime, func, text +from .base import Base + + +class RawDatas(Base): + __tablename__ = "col_test_raw_data" + + raw_dt = Column(TIMESTAMP, primary_key=True) + mg_equip_rtu_seq = Column(VARCHAR, primary_key=True) + acc_pg = Column(VARCHAR) + pg = Column(VARCHAR) + + +class Stat_pwr_rtu_1d_test(Base): + __tablename__ = "stat_pwr_rtu_1d_test" + + stat_dt = Column(TIMESTAMP, primary_key=True) + rtu_seq = Column(Integer, primary_key=True) + stat_value = Column(FLOAT) + + +class Stat_pwr_rtu_1d(Base): + __tablename__ = "stat_pwr_rtu_1d" + + stat_dt = Column(TIMESTAMP, primary_key=True) + rtu_seq = Column(Integer, primary_key=True) + stat_value = Column(FLOAT) + + +class Stat_weather_icsr_dc10tca_1d(Base): + __tablename__ = "stat_weather_icsr_dc10tca_1d" + + tm = Column(TIMESTAMP, primary_key=True) + icsr_station_id = Column(VARCHAR) + icsr = Column(VARCHAR) + dc10tca_station_id = Column(VARCHAR) + dc10tca = Column(VARCHAR) + update_dt = Column(TIMESTAMP, nullable=True) + + +class Raw_data_herit(Base): + __tablename__ = "raw_data_herit" + + seq = Column(Integer, primary_key=True, autoincrement="auto") + tm = Column(TIMESTAMP) + content_type = Column(VARCHAR) + x_hit_transactionid = Column(VARCHAR) + content_length = Column(VARCHAR) + body = Column(JSON) + + +class CardExpense(Base): + __tablename__ = "card_expenses" + + id = Column(Integer, primary_key=True, index=True) + jang = Column(String(1), nullable=False) + yeo = Column(String(1), nullable=False) + kim = Column(String(1), nullable=False) + choi = Column(String(1), nullable=False) + amount = Column(Numeric(10), nullable=False) + purpose = Column(String(255), nullable=False) + created_at = Column(DateTime, nullable=False) + diff --git a/fastapi/app/db/schemas.py b/fastapi/app/db/schemas.py new file mode 100644 index 0000000..5f8ded7 --- /dev/null +++ b/fastapi/app/db/schemas.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel +from datetime import datetime + + +class RawData(BaseModel): + raw_dt: str + mg_equip_rtu_seq: str + acc_pg: str + pg: str + + +class RtuGenerator(BaseModel): + month: float + day: float + hour: float + rtu_seq: float + icsr: float + dc10tca: float + + +class RawDataHerit(BaseModel): + tm: str + content_type: str + X_HIT_TransactionId: str + content_Length: str + body: dict + + +class CardExpenseCreate(BaseModel): + jang: str + yeo: str + kim: str + choi: str + amount: float + purpose: str + + +class CardExpense(BaseModel): + id: int + jang: str + yeo: str + kim: str + choi: str + amount: float + purpose: str + created_at: datetime + + class Config: + orm_mode = True + + +class DatePayload(BaseModel): + year: int + month: int \ No newline at end of file diff --git a/fastapi/app/error_request.log b/fastapi/app/error_request.log new file mode 100644 index 0000000..e69de29 diff --git a/fastapi/app/main.py b/fastapi/app/main.py new file mode 100644 index 0000000..af033b1 --- /dev/null +++ b/fastapi/app/main.py @@ -0,0 +1,83 @@ +import time + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import logging +import datetime + +from router.router_api import router as router_api +from router.card_service import router as router_card +from router.image_api import router as image_api +from router.manage_user_api import router as manage_user_api +from router.room_score_api import router as room_score_api + +# test +from router.test_service import router as test_service_api + +app = FastAPI() + +# docker로 수행하기 때문에 로그는 생략 +# logger = logging.getLogger() +# logger.setLevel(logging.INFO) +# file_handler = logging.FileHandler("testtest.log") +# logger.addHandler(file_handler) + +# ----------------------------------------------------------------------------------------------- + +# CORS 설정 +origins = [ + "http://localhost", + "http://localhost:3000", + "https://localhost", + "https://localhost:3000", + "http://eldsoft.com", + "http://eldsoft.com:3000", + "https://eldsoft.com", + "https://eldsoft.com:3000" +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# /api +app.include_router(router_api) + +# /api/card +app.include_router(router_card) + +# /images +app.include_router(image_api) + +# /user +app.include_router(manage_user_api) + +# /room/score +app.include_router(room_score_api) + + + + + +# /test +app.include_router(test_service_api) + + + +# /iot-kt +# app.include_router(router_iot_kt) +# /iot-lg +# app.include_router(router_iot_lg) + +@app.get("/ttt") +async def test_endpoint(data: dict): + return {"get received_data": data} + + +@app.post("/ttt") +async def test_endpoint(data: dict): + return {"received_data": data} diff --git a/fastapi/app/middleware/middleware.py b/fastapi/app/middleware/middleware.py new file mode 100644 index 0000000..2787e25 --- /dev/null +++ b/fastapi/app/middleware/middleware.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Request +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import Response +import time + + +class TimeHeaderMiddleware(BaseHTTPMiddleware) : + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + return await super().dispatch(request, call_next) diff --git a/fastapi/app/parse.py b/fastapi/app/parse.py new file mode 100644 index 0000000..0ae4efc --- /dev/null +++ b/fastapi/app/parse.py @@ -0,0 +1,167 @@ +{ + 'device_id': 'HERIT-HC1000N4-359689092518957', + 'parent_id': 'herit-gw_nbiot', + 'sid': '1', + 'msg': { + 'o': 'n', + 'e': [ + { + 'sv': '211', + 'ti': '1686883200000', + 'n': '/99/0/5' + }, { + 'sv': '55,57,51', + 'ti': '1686883200000', + 'n': '/99/0/6' + }, { + 'sv': '24,22,1,1', + 'ti': '1686883200000', + 'n': '/99/0/8' + }, { + 'sv': '24,24,0,0', + 'ti': '1686883200000', + 'n': '/99/0/9' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/99/0/10' + }, { + 'sv': '9778500', + 'ti': '1686883200000', + 'n': '/100/1/0' + }, { + 'sv': '551421', + 'ti': '1686883200000', + 'n': '/100/1/1' + }, { + 'sv': '7700', + 'ti': '1686883200000', + 'n': '/100/1/2' + }, { + 'sv': '361', + 'ti': '1686883200000', + 'n': '/100/1/3' + }, { + 'sv': '2413', + 'ti': '1686883200000', + 'n': '/100/1/4' + }, { + 'sv': '700', + 'ti': '1686883200000', + 'n': '/100/1/5' + }, { + 'sv': '291', + 'ti': '1686883200000', + 'n': '/100/1/6' + }, { + 'sv': '9.0', + 'ti': '1686883200000', + 'n': '/100/1/7' + }, { + 'sv': '2619.0', + 'ti': '1686883200000', + 'n': '/100/1/8' + }, { + 'sv': '235', + 'ti': '1686883200000', + 'n': '/100/1/9' + }, { + 'sv': '10.0', + 'ti': '1686883200000', + 'n': '/100/1/10' + }, { + 'sv': '2413.0', + 'ti': '1686883200000', + 'n': '/100/1/11' + }, { + 'sv': '98.1', + 'ti': '1686883200000', + 'n': '/100/1/12' + }, { + 'sv': '60.0', + 'ti': '1686883200000', + 'n': '/100/1/13' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/0' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/102/1/0' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/19' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/20' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/5' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/6' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/11' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/9' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/10' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/3' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/8' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/16' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/14' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/15' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/21' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/17' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/18' + }, { + 'sv': '0', + 'ti': '1686883200000', + 'n': '/101/1/22' + }, { + 'sv': '1', + 'ti': '1686883200000', + 'n': '/200/1/4' + }, { + 'sv': '1', + 'ti': '1686883200000', + 'n': '/200/1/5' + } + ] + } +} \ No newline at end of file diff --git a/fastapi/app/process/certification/cert_process.py b/fastapi/app/process/certification/cert_process.py new file mode 100644 index 0000000..532ff49 --- /dev/null +++ b/fastapi/app/process/certification/cert_process.py @@ -0,0 +1,161 @@ +from fastapi import Request +import json +from process.logger import logger +import jwt +import datetime +import time + +from common.config import SECRET_KEY + +async def cert_process_by_token(request: Request): + # 헤더에서 토큰 찾기 + auth_token = request.headers.get('auth-token') + # 헤더에 토큰이 없으면 인증 거절 처리 + if auth_token == None: + return { + "result": "FAIL", + "token": "" + } + else: + # 헤더에 토큰이 있으므로 DB에서 유효한 토큰 값인지 검증하기 + 1 + + +def create_jwt(user_seq: int, period: int): + # 현재 시간 + now = int(time.time()) + + # JWT의 페이로드 (클레임) + payload = { + "user_seq": user_seq, + "exp": now + period, # 만료 시간 + "iat": now, # 토큰 발행 시간 + } + + # JWT 생성 (HS256 알고리즘 사용) + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + + return token + + +def decode_jwt(token: str): + try: + # 토큰 디코드 + decoded_payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + return { + "result": "OK", + "data": decoded_payload + } + except jwt.ExpiredSignatureError: + return { + "result": "TOKEN_EXPIRED", + "error": "Token has expired" + } + except jwt.InvalidTokenError: + return { + "result": "FAIL", + "error": "Invalid token" + } + + + +# 인증정보 JWT토큰화 +def create_user_cert_seq_jwt(user_cert_seq: int, period: int): + # 현재 시간 + now = int(time.time()) + + # JWT의 페이로드 (클레임) + payload = { + "user_cert_seq": user_cert_seq, + "exp": now + period, # 만료 시간 + "iat": now, # 토큰 발행 시간 + } + + # JWT 생성 (HS256 알고리즘 사용) + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + + return token + + +# 인증서 갱신 +def renew_cert(request: Request): + try: + # 헤더에서 토큰 찾기 + auth_token = request.headers.get('auth-token') + decoded_payload = decode_jwt(auth_token) + if decoded_payload['result'] == 'OK': + decoded_payload = decoded_payload['data'] + user_seq = decoded_payload['user_seq'] + exp = decoded_payload['exp'] + iat = decoded_payload['iat'] + now = int(time.time()) + # 시간비교 유효한 토큰인지 확인 + if iat <= now and now <= exp: + return { + "result": "OK", + "data": make_auth_data(create_jwt(user_seq=user_seq, period=3600), 'Y') + } + else: + return { + "result": "TOKEN_EXPIRED", + "data": make_auth_data('', 'N'), + "msg": '인증서 토큰이 만료 되었습니다.' + } + elif decoded_payload['result'] == 'TOKEN_EXPIRED': + return { + "result": "TOKEN_EXPIRED", + "data": make_auth_data('', 'N'), + "msg": '인증서 토큰이 만료 되었습니다.' + } + else: + logger.error(f"token renew error. message: {e}") + return { + "result": "FAIL", + "data": make_auth_data('', 'N'), + "msg": decoded_payload['error'] + } + + + except Exception as e: + logger.error(f"token renew error. message: {e}") + return { + "result": "FAIL", + "data": make_auth_data('', 'N'), + "msg": '인증서 토큰 갱신에 실패했습니다.' + } + + +# 응답 auth 함수화 +def make_auth_data(token, tf): + return { + "renew_yn": tf, + "token": token + } + + +# 토큰으로 user_seq 가져오기 +def get_user_seq_by_token(token: str): + try: + payload = decode_jwt(token) + if payload['result'] == 'OK': + user_seq = payload['data']['user_seq'] + return { + "result": "OK", + "data":{ + "user_seq": user_seq + } + } + else: + return { + "result": "FAIL", + "msg": payload['error'] + } + + except Exception as e: + logger.error(f"token error. message: {e}") + return { + "result": "FAIL", + "msg": 'get_user_seq_by_token error' + } + + diff --git a/fastapi/app/process/email/email_user.py b/fastapi/app/process/email/email_user.py new file mode 100644 index 0000000..2a43244 --- /dev/null +++ b/fastapi/app/process/email/email_user.py @@ -0,0 +1,106 @@ +from fastapi import FastAPI, HTTPException, BackgroundTasks +from pydantic import BaseModel, EmailStr +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import os +import time +import datetime +from jinja2 import Template + +from process.logger import logger +from common.config import SMTP_PASSWORD, SMTP_PORT, SMTP_SERVER, SMTP_USERNAME + + + + + +# HTML 템플릿을 로드하고 렌더링 +def render_template(template_path, context): + with open(template_path, 'r') as file: + template = Template(file.read()) + return template.render(context) + + +# 메일 발송 함수 +def send_email(email_to: str, subject: str, html_content: str): + try: + msg = MIMEMultipart("alternative") + msg['From'] = SMTP_USERNAME + msg['To'] = email_to + msg['Subject'] = subject + + msg.attach(MIMEText(html_content, 'html')) + + server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + + # 이메일 발송 + server.sendmail(SMTP_USERNAME, email_to, msg.as_string()) + server.quit() + + return { + "result": 'OK', + "msg": "이메일 발송에 성공했습니다." + } + + except Exception as e: + logger.error(f"이메일 발송에 실패했습니다. error_msg: {e}") + return { + "result": 'FAIL', + "msg": "이메일 발송에 실패했습니다." + } + + +# 이메일 발송 API 엔드포인트 +def send_email_api(context): + try: + html_content = render_template(f"/app/process/email/{context['template_name']}", context) + + send_email_result = send_email(email_to=context['user_email'], subject=context['email_title'], html_content=html_content) + return send_email_result + + except Exception as e: + logger.error(f"이메일 발송에 실패했습니다. error_msg: {e}") + return { + "result": 'FAIL', + "msg": "이메일 발송에 실패했습니다." + } + + +# 아이디 찾기 이메일 발송 함수 +def email_find_id(user_email, info): + context = { + "email_title": "[ALLSCORE] 아이디가 발송되었습니다.", + "info": info, + "user_email": user_email, + "template_name": "find_id_template.html", + } + try: + return send_email_api(context) + except Exception as e: + logger.error(f"이메일 발송에 실패했습니다. error_msg: {e}") + return { + "result": 'FAIL', + "msg": "이메일 발송에 실패했습니다." + } + + + +# 비밀번호 찾기 이메일 발송 함수 +def email_find_password(user_email, info): + context = { + "email_title": "[유어라운드] 임시 비밀번호가 발급되었습니다.", + "info": info, + "user_email": user_email, + "template_name": "find_password_template.html", + } + try: + return send_email_api(context) + except Exception as e: + logger.error(f"이메일 발송에 실패했습니다. error_msg: {e}") + return { + "result": 'FAIL', + "msg": "이메일 발송에 실패했습니다." + } \ No newline at end of file diff --git a/fastapi/app/process/email/find_id_template.html b/fastapi/app/process/email/find_id_template.html new file mode 100644 index 0000000..19d334e --- /dev/null +++ b/fastapi/app/process/email/find_id_template.html @@ -0,0 +1,24 @@ + + + + + + Email Template + + + + + + + + + +
+

고객님의 아이디는 다음과 같습니다.

+

{{ info }}

+
+
+

본 메일은 발신 전용으로 회신되지 않습니다.

+
+ + diff --git a/fastapi/app/process/email/find_password_template.html b/fastapi/app/process/email/find_password_template.html new file mode 100644 index 0000000..c7ff349 --- /dev/null +++ b/fastapi/app/process/email/find_password_template.html @@ -0,0 +1,25 @@ + + + + + + Email Template + + + + + + + + + +
+

고객님의 임시 비밀번호는 다음과 같습니다.

+

{{ info }}

+

임시 비밀번호로 로그인 한 후 마이페이지에서 비밀번호를 변경해 주세요.

+
+
+

본 메일은 발신 전용으로 회신되지 않습니다.

+
+ + diff --git a/fastapi/app/process/logger.py b/fastapi/app/process/logger.py new file mode 100644 index 0000000..eaa6315 --- /dev/null +++ b/fastapi/app/process/logger.py @@ -0,0 +1,55 @@ +import logging +import sys +from logging.handlers import TimedRotatingFileHandler +import os +from datetime import datetime + +class CustomTimedRotatingFileHandler(TimedRotatingFileHandler): + def __init__(self, base_log_path, when="midnight", interval=1, backupCount=7): + self.base_log_path = base_log_path + # 초기화 시점에 파일 경로를 명시적으로 설정 + self.update_log_path() + super().__init__(self.baseFilename, when=when, interval=interval, backupCount=backupCount) + + def update_log_path(self): + current_time = datetime.now() + log_directory = os.path.join( + self.base_log_path, + current_time.strftime("%Y"), + current_time.strftime("%m") + ) + log_filename = current_time.strftime("%d_allscore.log") + + # 로그 파일 경로 설정 + self.baseFilename = os.path.join(log_directory, log_filename) + + # 디렉토리가 없으면 생성 + if not os.path.exists(log_directory): + os.makedirs(log_directory) + + def emit(self, record): + # 각 로그 기록 시점에서 경로를 다시 확인해 업데이트 + self.update_log_path() + super().emit(record) + +# 로거 설정 +logger = logging.getLogger("allscore_logger") +logger.setLevel(logging.DEBUG) + +# 핸들러 설정 (stdout으로 출력) +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(logging.DEBUG) + +# 로그 포맷 설정 +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +stream_handler.setFormatter(formatter) + +# 커스텀 파일 핸들러 설정 +base_log_path = "/app/logs" +file_handler = CustomTimedRotatingFileHandler(base_log_path=base_log_path, when="midnight", interval=1, backupCount=7) +file_handler.setLevel(logging.DEBUG) +file_handler.setFormatter(formatter) + +# 핸들러를 로거에 추가 +logger.addHandler(stream_handler) +logger.addHandler(file_handler) diff --git a/fastapi/app/process/response/response.py b/fastapi/app/process/response/response.py new file mode 100644 index 0000000..ce657a9 --- /dev/null +++ b/fastapi/app/process/response/response.py @@ -0,0 +1,96 @@ +from fastapi import Request +import json +from process.logger import logger +import jwt +import datetime +import time + +from process.certification import cert_process +from process.user import manage_user + + +# 성공 응답 +async def ok_res(auth_token, data, db): + try: + # 토큰에서 user_seq 찾기 + user_seq_result = cert_process.get_user_seq_by_token(token=auth_token['token']) + if user_seq_result['result'] == 'OK': + user_seq = user_seq_result['data']['user_seq'] + else: + return make_response('OK', auth_token['renew_yn'], 'NOMAL', auth_token['token'], '성공', '성공', data) + + return make_response('OK', auth_token['renew_yn'], '', auth_token['token'], '성공', '성공', data) + + except Exception as e: + logger.error(f"ok response error: {e}") + return make_response('ERROR', 'N', 'NOMAL', '', '응답 에러', '서버 장애가 발생했습니다.', data) + + +# 실패 응답 +async def fail_res(auth_token, auth_type, msg_title, msg_content, data): + try: + return make_response('FAIL', auth_token['renew_yn'], auth_type, auth_token['token'], msg_title, msg_content, data) + except Exception as e: + logger.error(f"ok response error: {e}") + return make_response('ERROR', 'N', 'NOMAL', '', '응답 에러', '서버 장애가 발생했습니다.', data) + + +# 에러 응답 +async def error_res(auth_token, auth_type, msg_title, msg_content, data): + try: + return make_response('ERROR', auth_token['renew_yn'], auth_type, auth_token['token'], msg_title, msg_content, data) + except Exception as e: + logger.error(f"ok response error: {e}") + return make_response('ERROR', 'N', 'NOMAL', '', '응답 에러', '서버 장애가 발생했습니다.', data) + + + +# 응답 패턴 +def make_response(result, renew_yn, auth_type, token, msg_title, msg_content, data): + return { + "result": result, # OK, FAIL, ERROR + "auth": { + "renew_yn": renew_yn, + "type": auth_type, + "token": token, + }, + "response_info": { + "msg_type": result, # OK, FAIL, ERROR + "msg_title": msg_title, + "msg_content": msg_content, + }, + "data": data, + } + + + +# # 기존 응답 방식 +# { +# "result": "FAIL", +# "msg": fail_msg, +# "auth": { +# "auth-token": { +# "renew_yn": 'Y', +# "token": '토큰값' +# } +# }, +# "data": {} +# } + +# # 새로운 응답 방식 +# { +# "result": "FAIL", # OK, FAIL, ERROR +# "data": { +# "auth": { +# "renew_yn": 'Y', +# "type": 'ADMIN', +# "token": '토큰값' +# }, +# "response_info": { +# "msg_type": 'FAIL', # OK, FAIL, ERROR +# "msg_title": '제목', +# "msg_content": '내용', +# }, +# "data": '데이터', +# } +# } \ No newline at end of file diff --git a/fastapi/app/process/room/room_score.py b/fastapi/app/process/room/room_score.py new file mode 100644 index 0000000..6c36643 --- /dev/null +++ b/fastapi/app/process/room/room_score.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from db import crud_room_score +from sqlalchemy.orm import Session +from db.base import get_db +from db import models, schemas +from process.logger import logger + +# 방 만들기 +async def create_room(data, db): + db_result_ori = crud_room_score.create_room(data=data, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 일치하는 유저가 있는지 확인 + if len(db_result) == 0: + return { + "result": "FAIL" + } + else: + return { + "result": "OK", + "user_seq": db_result[0] + } + + + + + + + diff --git a/fastapi/app/process/user/manage_user.py b/fastapi/app/process/user/manage_user.py new file mode 100644 index 0000000..38920a1 --- /dev/null +++ b/fastapi/app/process/user/manage_user.py @@ -0,0 +1,240 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from db import crud_user +from sqlalchemy.orm import Session +from db.base import get_db +from db import models, schemas +from process.logger import logger + +# 로그인 수행 +async def do_login(user_id, user_pw, db): + # DB에서 유저 정보 확인 + db_result_ori = crud_user.do_login(user_id=user_id, user_pw=user_pw, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 일치하는 유저가 있는지 확인 + if len(db_result) == 0: + return { + "result": "FAIL" + } + else: + return { + "result": "OK", + "user_seq": db_result[0] + } + + +# 마지막 로그인 시간 업데이트 +async def update_last_login_dt(user_seq, db): + # 마지막 로그인 시간 업데이트 + db_result = crud_user.update_last_login_dt(user_seq=user_seq, db=db) + + if db_result: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + +# 아이디 찾기(닉네임, 이메일) +async def find_id_by_name_email(nickname, user_email, db): + # DB에서 회원정보 찾기 + db_result_ori = crud_user.find_id_by_name_email(nickname=nickname, user_email=user_email, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 일치하는 유저가 있는지 확인 + if len(db_result) == 0: + return { + "result": "FAIL" + } + else: + db_result = db_result[0] + return { + "result": "OK", + "user_seq": db_result[0], + "user_id": db_result[1] + } + + +# 아이디 찾기(user_seq) +async def find_id_by_user_seq(user_seq, db): + # DB에서 회원정보 찾기 + db_result_ori = crud_user.find_id_by_user_seq(user_seq=user_seq, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 일치하는 유저가 있는지 확인 + if len(db_result) == 0: + return { + "result": "FAIL" + } + else: + db_result = db_result[0] + return { + "result": "OK", + "user_id": db_result[0], + "user_email": db_result[1] + } + + +# 이메일 발송 3회 이상 됐는지 확인 +async def select_send_email_cnt(user_email, db): + db_result_ori = crud_user.select_send_email_cnt(user_email=user_email, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + db_result = db_result[0][0] + return { + "result": "OK", + "send_email_cnt": db_result + } + + +# 이메일 발송내역 기록(하루 3회만 발송가능하게 하기 위함) +async def insert_send_email_info(user_seq, cert_type, user_info, cert_code, db): + db_result = crud_user.insert_send_email_info(user_seq=user_seq, cert_type=cert_type, user_info=user_info, cert_code=cert_code, db=db) + + if db_result: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + +# 비밀번호 찾기 수행 +async def find_password_by_id_email(user_id, user_email, db): + # DB에서 회원정보 찾기 + db_result_ori = crud_user.find_password_by_id_email(user_id=user_id, user_email=user_email, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 일치하는 유저가 있는지 확인 + if len(db_result) == 0: + return { + "result": "FAIL" + } + else: + return { + "result": "OK", + "user_seq": db_result[0] + } + + +# 비밀번호 업데이트 수행 +async def update_new_password(user_seq, new_pw, new_solt, db): + # 회원정보 업데이트 + db_result = crud_user.update_new_password(user_seq=user_seq, new_pw=new_pw, new_solt=new_solt, db=db) + + if db_result: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + +# 유저 회원가입 진행 +async def insert_new_user(user_info, db): + # DB에서 신규 유저 등록 + db_result_manage_user = crud_user.insert_new_user(user_info=user_info, db=db) + + # 인증코드 입력 결과 + if not db_result_manage_user: + return { + "result": "FAIL" + } + else: + return { + "result": "OK", + } + + +# 마이페이지 - 유저 정보 가져오기 +async def get_my_info_by_user_seq(user_seq, db): + # DB에서 유저 정보 가져오기 + db_result_ori = crud_user.get_my_info_by_user_seq(user_seq=user_seq, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + # 데이터 1개만 존재하는지 확인 + if len(db_result) == 1: + # 데이터 key value 구분해주기 + db_result = db_result[0] + return { + "result": "OK", + "data": { + "nickname": db_result[0], + "user_email": db_result[1], + "department": db_result[2], + "profile_img": db_result[3], + "introduce_myself": db_result[4] + } + } + return { + "result": "FAIL" + } + + +# 현재 비밀번호 일치 확인 +async def check_current_user_pw(user_seq, user_pw, db): + db_result_ori = crud_user.check_current_user_pw(user_seq=user_seq, user_pw=user_pw, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + if db_result[0][0] == 1: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + +# 회원정보 업데이트 +async def update_user_info(user_info, db): + # 회원정보 업데이트 + db_result = crud_user.update_user_info(user_info=user_info, db=db) + + if db_result: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + +# 회원 탈퇴 처리 +async def user_withdraw(user_seq, db): + db_result = crud_user.user_withdraw(user_seq=user_seq, db=db) + + if db_result: + return { + "result": "OK" + } + else: + return { + "result": "FAIL", + } + + + + diff --git a/fastapi/app/process/user/manage_user_pattern.py b/fastapi/app/process/user/manage_user_pattern.py new file mode 100644 index 0000000..c9b8f40 --- /dev/null +++ b/fastapi/app/process/user/manage_user_pattern.py @@ -0,0 +1,116 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from db import crud_user +from sqlalchemy.orm import Session +from db import models, schemas +from process.logger import logger +import re + + +# 회원가입 입력항목 확인 +def check_mandatory_insert_list(user_info): + # user_id 확인 + if not validate_user_id(user_id=user_info['user_id']): + return '아이디 규칙이 맞지 않습니다.' + + # user_pw 확인 + if not validate_user_pw(user_pw=user_info['user_pw']): + return '비밀번호 암호화가 되지 않았습니다.' + + # nickname 확인 + if not validate_nickname(nickname=user_info['nickname']): + return '사용자 닉네임 규칙이 맞지 않습니다.' + + # user_email 확인 + if not validate_user_email(user_email=user_info['user_email']): + return '이메일 규칙이 맞지 않습니다.' + + # introduce_myself 확인 + if len(user_info['introduce_myself']) > 2000: + return '자기소개 글은 2000byte까지만 입력 가능합니다.' + + # mandatory_terms 확인 + if user_info['mandatory_terms_yn'] != 'Y': + return '필수약관항목에 대해 동의 하지 않았습니다.' + + # 이상없음 + return None + + +# 회원정보 수정 입력값 검증 +def check_new_data(user_info): + # user_pw 확인 + if user_info['user_pw_change_yn'] == 'Y': + if not validate_user_pw(user_pw=user_info['user_pw']): + return '비밀번호 암호화가 되지 않았습니다.' + + # nickname 확인 + if not validate_nickname(nickname=user_info['nickname']): + return '사용자 닉네임 규칙이 맞지 않습니다.' + + # user_email 확인 + if not validate_user_email(user_email=user_info['user_email']): + return '이메일 규칙이 맞지 않습니다.' + + # profile_img 확인 + if not starts_with_user_dir(user_info['profile_img']): + return '프로필 이미지 경로가 정확하지 않습니다.' + + # introduce_myself 확인 + if len(user_info['introduce_myself']) > 2000: + return '자기소개 글은 2000byte까지만 입력 가능합니다.' + + # 이상없음 + return None + + +# user_id 정규식 검증 +def validate_user_id(user_id: str) -> bool: + pattern = r'^[a-zA-Z0-9]{6,20}$' + return bool(re.match(pattern, user_id)) + + +# user_pw sha256 검증 +def validate_user_pw(user_pw: str) -> bool: + return True if len(user_pw) == 64 else False + + +# nickname 정규식 검증 +def validate_nickname(nickname: str) -> bool: + # 바이트 계산 + byte_length = sum(2 if ord(char) > 127 else 1 for char in nickname) + + # 정규식으로 한글, 영어, 숫자만 허용 + pattern = re.compile(r'^[가-힣a-zA-Z0-9]+$') + + if pattern.match(nickname) and 2 <= byte_length <= 20: + return True + return False + + +# user_email 정규식 검증 +def validate_user_email(user_email: str) -> bool: + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return bool(re.match(pattern, user_email)) + + +# user_phone 정규식 검증 +def validate_user_phone(user_phone: str) -> bool: + pattern = r'^010\d{8}$' + return bool(re.match(pattern, user_phone)) + + +# 유저 아이디 중복 검사 +async def is_valid_user_id_by_user_id(user_id, db): + # DB에서 user_id 중복 확인하기 + db_result_ori = crud_user.is_valid_user_id_by_user_id(user_id=user_id, db=db) + db_result = [] + for db_data in db_result_ori: + db_result.append(db_data) + + return True if len(db_result) == 0 else False + + +# profile_img 파일명이 /user/temp_dir/profile_img/IMG 로 시작하는지 검증 +def starts_with_user_dir(value: str) -> bool: + prefix = '/user/temp_dir/profile_img/IMG' + return value.startswith(prefix) \ No newline at end of file diff --git a/fastapi/app/requirements.txt b/fastapi/app/requirements.txt new file mode 100644 index 0000000..fdb1f8c --- /dev/null +++ b/fastapi/app/requirements.txt @@ -0,0 +1,17 @@ +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +SQLAlchemy==1.3.15 +psycopg2==2.9.3 +matplotlib==3.5.0 +numpy==1.21.4 +pandas==1.3.5 +typing +kafka-python==2.0.2 +openpyxl==3.0.7 +aiofiles==0.6.0 +pyjwt==2.4.0 +pydantic[email]>=1.8.0,<2.0.0 +Jinja2>=3.1.2,<4.0.0 +requests>=2.25.0,<3.0.0 +python-multipart==0.0.6 # python-multipart 추가 diff --git a/fastapi/app/router/__pycache__/card_service.cpython-38.pyc b/fastapi/app/router/__pycache__/card_service.cpython-38.pyc new file mode 100644 index 0000000..4adaf2c Binary files /dev/null and b/fastapi/app/router/__pycache__/card_service.cpython-38.pyc differ diff --git a/fastapi/app/router/__pycache__/router_api.cpython-38.pyc b/fastapi/app/router/__pycache__/router_api.cpython-38.pyc new file mode 100644 index 0000000..cd30064 Binary files /dev/null and b/fastapi/app/router/__pycache__/router_api.cpython-38.pyc differ diff --git a/fastapi/app/router/card_service.py b/fastapi/app/router/card_service.py new file mode 100644 index 0000000..0d4191f --- /dev/null +++ b/fastapi/app/router/card_service.py @@ -0,0 +1,70 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from sqlalchemy.orm import Session +from fastapi.security import APIKeyHeader +from typing import Union, Optional, List +from typing_extensions import Annotated +from db import models, schemas, crud +import json +import logging +import datetime +from kafka import KafkaProducer +from fastapi.responses import FileResponse, StreamingResponse +import io +import openpyxl + +from db.schemas import RawData, RtuGenerator +from db.base import get_db +from db.models import RawDatas, Raw_data_herit +import pandas as pd + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/api/card", + tags=["api", "card"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- + +async def get_body(request: Request): + return await request.body() + + +@router.post("/regist", response_model=schemas.CardExpense) +def create_card_expense(card_expense: schemas.CardExpenseCreate, db: Session = Depends(get_db)): + return crud.create_card_expense(db=db, card_expense=card_expense) + + +@router.post("/excel", response_model=schemas.CardExpense) +async def create_excel(payload: schemas.DatePayload, db: Session = Depends(get_db)): + results = crud.get_card_expenses_by_date(db, payload.year, payload.month) + + if not results: + raise HTTPException(status_code=404, detail="No data found for the specified month") + + # Convert results to pandas DataFrame + df = pd.DataFrame([dict(result) for result in results]) + + # Create an Excel file in memory + output = io.BytesIO() + writer = pd.ExcelWriter(output, engine='openpyxl') + df.to_excel(writer, index=False, sheet_name='Summary') + writer.save() + output.seek(0) + + # Return the Excel file + response = StreamingResponse(output, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response.headers["Content-Disposition"] = "attachment; filename=summary.xlsx" + return response + + +# 테스트 URL +@router.post("/test") +async def col_raw_data_from_herit( + body: bytes = Depends(get_body) + ): + return { + "reason": "OK2", + "body": body + } \ No newline at end of file diff --git a/fastapi/app/router/image_api.py b/fastapi/app/router/image_api.py new file mode 100644 index 0000000..a518d3f --- /dev/null +++ b/fastapi/app/router/image_api.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +# from db import crud_main +from sqlalchemy.orm import Session +from fastapi.security import APIKeyHeader +from typing import Union, Optional, List +from typing_extensions import Annotated +from db import models, schemas +import datetime +from fastapi.responses import FileResponse +import os +from process.logger import logger + +from db.base import get_db +import pandas as pd + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/images", + tags=["main"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- + +async def get_body(request: Request): + return await request.body() + + +# 이미지 제공 +@router.post("/{path1}/{path2}/{path3}/{file_name}") +async def get_image(path1: str, path2: str, path3: str, file_name: str): + # 이미지 파일 경로 설정 (예: 서버의 특정 디렉토리) + image_path = os.path.join("images", path1, path2, path3, file_name) + + # 이미지 파일이 존재하는지 확인 + if not os.path.exists(image_path): + return {"error": "Image not found"} + + # FileResponse를 사용하여 이미지 파일 반환 + return FileResponse(image_path) + + +# 이미지 제공 +@router.get("/{path1}/{path2}/{path3}/{file_name}") +async def get_image1(path1: str, path2: str, path3: str, file_name: str): + # if path1 != 'email': + # return {"error": "path is not allow"} + # 이미지 파일 경로 설정 (예: 서버의 특정 디렉토리) + image_path = os.path.join("images", path1, path2, path3, file_name) + + # 이미지 파일이 존재하는지 확인 + if not os.path.exists(image_path): + return {"error": "Image not found"} + + # FileResponse를 사용하여 이미지 파일 반환 + return FileResponse(image_path) \ No newline at end of file diff --git a/fastapi/app/router/manage_user_api.py b/fastapi/app/router/manage_user_api.py new file mode 100644 index 0000000..4d3a7a4 --- /dev/null +++ b/fastapi/app/router/manage_user_api.py @@ -0,0 +1,557 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request, File, UploadFile, Form +from sqlalchemy.orm import Session +from fastapi.security import APIKeyHeader +from typing import Union, Optional, List +from typing_extensions import Annotated +from db import models, schemas, crud +import json +import logging +import datetime +from kafka import KafkaProducer +from fastapi.responses import FileResponse, StreamingResponse +import os +import openpyxl +import time +import random +import string + +from db.schemas import RawData, RtuGenerator +from db.base import get_db + +from process.logger import logger + +from process.certification import cert_process +from process.response import response +from process.user import manage_user +from process.user import manage_user_pattern +from process.email import email_user + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/user", + tags=["user"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- + +async def get_body(request: Request): + return await request.body() + +#================================================================================================== +# 로그인 +#================================================================================================== +@router.post("/login") +async def login(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + auth_token = auth_token['data'] + # body에서 ID, PW 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_id = body['user_id'] + user_pw = body['user_pw'] + + # ID, PW로 로그인 시도 + login_result = await manage_user.do_login(user_id= user_id, user_pw= user_pw, db=db) + + # 로그인 결과에 따른 응답 + if login_result['result'] == 'OK': + user_seq = (login_result['user_seq'])[0] + # 마지막 로그인 시간 업데이트 + last_login_dt_result = await manage_user.update_last_login_dt(user_seq=user_seq, db=db) + if last_login_dt_result['result'] == 'OK': + auth_token = { + "renew_yn": 'Y', + "token": cert_process.create_jwt(user_seq=user_seq, period=3600) + } + return await response.ok_res(auth_token=auth_token, data={}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 실패', msg_content='로그인 정보 업데이트에 실패했습니다.', data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 실패', msg_content='회원정보를 다시 확인해주세요.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/login\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 아이디 찾기 +#================================================================================================== +@router.post("/find/id") +async def find_id(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + auth_token = auth_token['data'] + # body에서 ID, PW 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + nickname = body['nickname'] + user_email = body['user_email'] + + # 닉네임 패턴 검증 + if not manage_user_pattern.validate_nickname(nickname=nickname): + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이름 확인', msg_content='이름을 정확히 입력해주세요.', data={}) + # 이메일 패턴 검증 + if not manage_user_pattern.validate_user_email(user_email=user_email): + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 확인', msg_content='이메일 주소를 정확히 입력해주세요.', data={}) + + # 닉네임, EMAIL로 아이디 찾기 시도 + find_id_result = await manage_user.find_id_by_name_email(nickname=nickname, user_email=user_email, db=db) + + # 아이디 찾기 시도 결과에 따른 응답 + if find_id_result['result'] == 'OK': + # user_seq, user_id 확인 + user_seq = find_id_result['user_seq'] + user_id = find_id_result['user_id'] + # ID 마스킹 처리 + user_id = user_id[:int(len(user_id)/2)] + ''.join(['*' for _ in range(int(len(user_id)/2))]) + + data = { + "auth": cert_process.create_jwt(user_seq=user_seq, period=600), + "user_id": user_id, + } + return await response.ok_res(auth_token=auth_token, data=data, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='아이디 찾기 실패', msg_content='회원정보를 다시 확인해주세요.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/find/id\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 아이디 이메일 전송 +#================================================================================================== +@router.post("/find/id/full") +async def find_receive_by_eamil(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + auth_token = auth_token['data'] + # body에서 ID, PW 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + auth = body['auth'] + auth_data = cert_process.decode_jwt(auth) + if auth_data['result'] == 'OK': + auth_data = auth_data['data'] + user_seq = auth_data['user_seq'] + exp = auth_data['exp'] + iat = auth_data['iat'] + current_time = int(time.time()) + if not(iat <= current_time and current_time <= exp): + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='토큰 유효기간이 만료되었습니다.', data={}) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + # user_seq로 아이디 찾기 시도 + find_id_result = await manage_user.find_id_by_user_seq(user_seq=user_seq, db=db) + + # 아이디 찾기 시도 결과에 따른 응답 + if find_id_result['result'] == 'OK': + # user_id, user_email 확인 + user_id = find_id_result['user_id'] + user_email = find_id_result['user_email'] + # 이메일 발송 3회 이상 됐는지 확인 + send_email_cnt_result = await manage_user.select_send_email_cnt(user_email=user_email, db=db) + if send_email_cnt_result['result'] == 'OK': + send_email_cnt = send_email_cnt_result['send_email_cnt'] + if send_email_cnt >= 3: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='이메일 발송은 하루 3번까지 가능합니다.', data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='이메일 발송 실패했습니다.', data={}) + # 이메일 발송 인증 내역 입력 + insert_email_cert_result = await manage_user.insert_send_email_info(user_seq=user_seq, cert_type='email', user_info=user_email, cert_code=user_id, db=db) + if insert_email_cert_result['result'] == 'FAIL': + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='이메일 발송 실패했습니다.', data={}) + # 이메일로 유저 아이디 발송해주기 + send_email_result = email_user.email_find_id(user_email=user_email, info=user_id) + if send_email_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={"send_email_result": True}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content=send_email_result['msg'], data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='이메일 발송 실패했습니다.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/find/id/full\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='아이디 찾기 에러', msg_content='아이디 찾기 이메일 발송 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 비밀번호 찾기 +#================================================================================================== +@router.post("/find/password") +async def find_pw(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + auth_token = auth_token['data'] + # body에서 ID, PW 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_id = body['user_id'] + user_email = body['user_email'] + # 아이디 패턴 검증 + if not manage_user_pattern.validate_user_id(user_id=user_id): + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='아이디 확인', msg_content='아이디를 정확히 입력해주세요.', data={}) + # 이메일 패턴 검증 + if not manage_user_pattern.validate_user_email(user_email=user_email): + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 확인', msg_content='이메일 주소를 정확히 입력해주세요.', data={}) + # 이메일 발송 3회 이상 됐는지 확인 + send_email_cnt_result = await manage_user.select_send_email_cnt(user_email=user_email, db=db) + if send_email_cnt_result['result'] == 'OK': + send_email_cnt = send_email_cnt_result['send_email_cnt'] + if send_email_cnt >= 3: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content='이메일 발송은 하루 3번까지 가능합니다.', data={}) + # ID, EMAIL로 비밀번호 찾기 시도 + find_password_result = await manage_user.find_password_by_id_email(user_id= user_id, user_email= user_email, db=db) + logger.debug(f"test1") + + # 비밀번호 찾기 시도 결과에 따른 응답 + if find_password_result['result'] == 'OK': + # user_seq 확인, 신규 PW&SOLT 생성하기 + user_seq = (find_password_result['user_seq'])[0] + new_pw = generate_random_string(20) + new_solt = generate_random_string(10) + # DB에 신규로 생성된 비밀번호 업데이트 하기 + update_pw_result = await manage_user.update_new_password(user_seq=user_seq, new_pw=new_pw, new_solt=new_solt, db=db) + logger.debug(f"test2") + if update_pw_result['result'] == 'OK': + # DB 업데이트가 성공하면 이메일로 유저 임시 비밀번호 발송해주기 + send_email_result = email_user.email_find_password(user_email=user_email, info=new_pw) + logger.debug(f"test3") + if send_email_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={"send_email_result": send_email_result}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이메일 발송 실패', msg_content=send_email_result['msg'], data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='비밀번호 업데이트 실패', msg_content='임시 비밀번호 업데이트에 실패했습니다.', data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='비밀번호 찾기 실패', msg_content='회원정보를 다시 확인해주세요.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/find/password\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 회원가입 +#================================================================================================== +@router.post("/signup") +async def signup(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + auth_token = auth_token['data'] + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + # user_id 유효성 검사 + is_valid_user_id = await manage_user_pattern.is_valid_user_id_by_user_id(user_id=body['user_id'], db=db) + fail_msg = None if is_valid_user_id else '아이디가 이미 존재합니다.' + if fail_msg is not None: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원가입 실패', msg_content=fail_msg, data={}) + + # 필수 입력항목 패턴 확인 + mandatory_list_verify_msg = manage_user_pattern.check_mandatory_insert_list(user_info=body) + if mandatory_list_verify_msg is not None: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원가입 실패', msg_content=mandatory_list_verify_msg, data={}) + + # 회원가입 DB 입력 + body['user_pw_solt'] = generate_random_string(10) # 비밀번호 SOLT 생성 + insert_new_user_result = await manage_user.insert_new_user(user_info=body, db=db) + + if insert_new_user_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원가입 실패', msg_content='회원가입에 실패했습니다. 입력하신 정보를 다시 확인해주세요.', data={}) + + + except Exception as e: + logger.error(f"request error. URL: /user/signup\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 프로필 이미지 등록 +#================================================================================================== +@router.post("/update/profile/img") +async def update_profile_img(request: Request, body: str = Form(...), file: UploadFile = File(...), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + if auth_token['result'] == 'OK': + auth_token = auth_token['data'] + elif auth_token['result'] == 'TOKEN_EXPIRED': + raise token_expired_return_process(auth_token['msg']) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + # 파일 읽기 + content = await file.read() + # 파일 크기 제한 + MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB + file_size = len(content) # 파일 크기 확인 + if file_size > MAX_FILE_SIZE: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이미지 업로드 실패', msg_content='이미지 파일 크기는 5MB까지 가능합니다.', data={}) + + # body에 들어있는 이미지명 처리 + user_seq_result = cert_process.get_user_seq_by_token(auth_token['token']) + if user_seq_result['result'] == 'OK': + # user_seq 추출 + user_seq = user_seq_result['data']['user_seq'] + # 현재 시간 가져오기 YYYYMMDDHHMMSSsss + now_time = get_now_time_str() + # 확장자 확인하고 텍스트 만들기 + extension = os.path.splitext(file.filename)[1].lower() + if extension not in ['.jpg', 'jpeg', '.png', 'bmp']: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이미지 업로드 실패', msg_content='지원하지 않는 확장자입니다.', data={}) + # IMG_(user_seq)_현재시간 으로 파일명 만들기 + file_name = f"IMG_{user_seq}_{now_time}{extension}" + # 파일 업로드 코드 시작 + # 경로 존재하지 않으면 생성하기 + UPLOAD_DIRECTORY = f"images/user/temp_dir/profile_img/" + if not os.path.exists(UPLOAD_DIRECTORY): + os.makedirs(UPLOAD_DIRECTORY) + # 파일 저장 경로 생성 + file_path = os.path.join(UPLOAD_DIRECTORY, file_name) + # 파일을 서버에 저장 + with open(file_path, "wb") as buffer: + buffer.write(content) + # 실제로 잘 저장됐는지 확인 + if os.path.exists(file_path) and os.path.getsize(file_path) > 0: + return await response.ok_res(auth_token=auth_token, data={"img_src": '/'+file_path}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='이미지 업로드 실패', msg_content='이미지 업로드에 실패했습니다.', data={}) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/update/profile/img\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 내 정보 가져오기 +#================================================================================================== +@router.post("/myinfo") +async def update_user_info(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + if auth_token['result'] == 'OK': + auth_token = auth_token['data'] + elif auth_token['result'] == 'TOKEN_EXPIRED': + raise token_expired_return_process(auth_token['msg']) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_seq_result = cert_process.get_user_seq_by_token(token=auth_token['token']) + if user_seq_result["result"] == 'OK': + user_seq = user_seq_result['data']['user_seq'] + # 유저 정보 가져오기 + user_data = await manage_user.get_my_info_by_user_seq(user_seq=user_seq, db=db) + + if user_data['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data=user_data['data'], db=db) + + logger.error(f"request error. URL: /user/myinfo\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='마이페이지 내정보 가져오기 에러', msg_content='마이페이지 데이터 조회중 에러가 발생했습니다.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /user/myinfo\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 유저 정보 수정하기 +#================================================================================================== +@router.post("/update/user/info") +async def update_user_info(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + if auth_token['result'] == 'OK': + auth_token = auth_token['data'] + elif auth_token['result'] == 'TOKEN_EXPIRED': + raise token_expired_return_process(auth_token['msg']) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_seq_result = cert_process.get_user_seq_by_token(token=auth_token['token']) + if user_seq_result["result"] == 'OK': + user_seq = user_seq_result['data']['user_seq'] + body['user_seq'] = user_seq + # 현재 비밀번호 일치하는지 확인 + current_pw_correct_result = await manage_user.check_current_user_pw(user_seq=user_seq, user_pw=body['user_pw'], db=db) + if current_pw_correct_result['result'] == 'FAIL': + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='현재 비밀번호 인증 실패', msg_content='현재 비밀번호가 일치하지 않습니다.', data={}) + + # 입력된 값 패턴 검증 + new_data_pattern_result_msg = manage_user_pattern.check_new_data(user_info=body) + if new_data_pattern_result_msg is not None: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원정보 수정 실패', msg_content=new_data_pattern_result_msg, data={}) + + # 유저 정보 가져오기 + user_data = await manage_user.get_my_info_by_user_seq(user_seq=user_seq, db=db) + if user_data['result'] == 'OK': + user_data = user_data['data'] + else: + return response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='정보 가져오기 에러', msg_content='유저 정보 가져오기에 실패했습니다.', data={}) + # 변경된 항목들 확인하기 + # user_pw 변경 됐는지 확인 + if body['user_pw_change_yn'] == 'Y': + body['user_pw_solt'] = generate_random_string(10) + # nickname 변경 됐는지 확인 + body['nickname'] = body['nickname'] if body['nickname'] != user_data['nickname'] else user_data['nickname'] + # user_email 변경 됐는지 확인 + body['user_email'] = body['user_email'] if body['user_email'] != user_data['user_email'] else user_data['user_email'] + # department 변경 됐는지 확인 + body['department'] = body['department'] if body['department'] != user_data['department'] else user_data['department'] + # profile_img 변경 됐는지 확인 + body['profile_img'] = body['profile_img'] if body['profile_img'] != user_data['profile_img'] else user_data['profile_img'] + # profile_img 변경 됐는지 확인 + body['introduce_myself'] = body['introduce_myself'] if body['introduce_myself'] != user_data['introduce_myself'] else user_data['introduce_myself'] + + # 변경된 정보에 대해서만 업데이트하기 + update_result = await manage_user.update_user_info(user_info=body, db=db) + if update_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={}, db=db) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원정보 변경 실패', msg_content='회원정보 변경 실패했습니다. 관리자에게 문의해주세요.', data={}) + else: + logger.error(f"request error. URL: /user/update/user/info\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + + + except Exception as e: + logger.error(f"request error. URL: /user/update/user/info\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + +#================================================================================================== +# 회원탈퇴 +#================================================================================================== +@router.post("/withdraw/user") +async def withdraw_user(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + if auth_token['result'] == 'OK': + auth_token = auth_token['data'] + elif auth_token['result'] == 'TOKEN_EXPIRED': + raise token_expired_return_process(auth_token['msg']) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_pw = body['user_pw'] + + user_seq_result = cert_process.get_user_seq_by_token(token=auth_token['token']) + if user_seq_result["result"] == 'OK': + user_seq = user_seq_result['data']['user_seq'] + # 현재 비밀번호 일치하는지 확인 + current_pw_correct_result = await manage_user.check_current_user_pw(user_seq=user_seq, user_pw=user_pw, db=db) + if current_pw_correct_result['result'] == 'OK': + # 회원 탈퇴 처리 + withdraw_result = await manage_user.user_withdraw(user_seq=user_seq, db=db) + if withdraw_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={}, db=db) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='회원탈퇴 에러', msg_content='회원탈퇴 처리중 에러가 발생헀습니다. 관리자에게 문의해주세요.', data={}) + else: + return await response.fail_res(auth_token=auth_token, auth_type='NOMAL', msg_title='현재 비밀번호 인증 실패', msg_content='현재 비밀번호가 일치하지 않습니다.', data={}) + else: + logger.error(f"request error. URL: /user/withdraw/user\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + + except Exception as e: + logger.error(f"request error. URL: /user/withdraw/user\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + + + + + + +#================================================================================================== +# 필요한 함수 +#================================================================================================== +# N자 대문자, 소문자, 숫자 조합 랜덤 발생기 +def generate_random_string(length=10): + # 영문 대문자, 소문자, 숫자를 모두 포함한 문자 집합 + characters = string.ascii_letters + string.digits + # 지정된 길이만큼 랜덤하게 선택하여 문자열 생성 + random_string = ''.join(random.choice(characters) for _ in range(length)) + return random_string + + +# N자 숫자 조합 랜덤 발생기 +def generate_random_number(length=6): + characters = string.digits + # 지정된 길이만큼 랜덤하게 선택하여 문자열 생성 + random_string = ''.join(random.choice(characters) for _ in range(length)) + return random_string + + +def token_expired_return_process(fail_msg): + logger.error(f"request fail: {fail_msg}") + return HTTPException( + status_code=401, + detail=f"{fail_msg}" + ) + + +# 현재 시간 STR로 가져오기 +def get_now_time_str(): + # 현재 시간 가져오기 + now = datetime.datetime.now() + + # YYYYMMDDHHMMSSsss 형식으로 포맷 + formatted_time = now.strftime('%Y%m%d%H%M%S%f')[:17] + + return formatted_time \ No newline at end of file diff --git a/fastapi/app/router/room_score_api.py b/fastapi/app/router/room_score_api.py new file mode 100644 index 0000000..0111151 --- /dev/null +++ b/fastapi/app/router/room_score_api.py @@ -0,0 +1,93 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from sqlalchemy.orm import Session +from fastapi.security import APIKeyHeader +from typing import Union, Optional, List +from typing_extensions import Annotated +from db import models, schemas, crud +import json +import logging +import datetime +from kafka import KafkaProducer +from fastapi.responses import FileResponse, StreamingResponse +import io +import openpyxl +import time + +from db.schemas import RawData, RtuGenerator +from db.base import get_db +from db.models import RawDatas, Raw_data_herit +import pandas as pd + +from process.logger import logger + +from process.certification import cert_process +from process.response import response +from process.room import room_score + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/room/score", + tags=["room", "score"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- + +async def get_body(request: Request): + return await request.body() + +#================================================================================================== +# 방 생성하기 +#================================================================================================== +@router.post("/create/room") +async def login(request: Request, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # 인증서 갱신 + auth_token = cert_process.renew_cert(request=request) + if auth_token['result'] == 'OK': + auth_token = auth_token['data'] + elif auth_token['result'] == 'TOKEN_EXPIRED': + raise token_expired_return_process(auth_token['msg']) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='데이터 에러', msg_content='데이터 처리 장애가 발생했습니다. 요청정보를 정확히 입력했는지 확인해주세요.', data={}) + + user_seq_result = cert_process.get_user_seq_by_token(token=auth_token['token']) + if user_seq_result["result"] == 'OK': + user_seq = user_seq_result['data']['user_seq'] + body['user_seq'] = user_seq + # 방 생성 + create_room_result = await room_score.create_room(data=body) + if create_room_result['result'] == 'OK': + return await response.ok_res(auth_token=auth_token, data={}, db=db) + else: + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='방생성 에러', msg_content='방 생성중 에러가 발생했습니다. 관리자에게 문의해주세요.', data={}) + else: + logger.error(f"request error. URL: /user/withdraw/user\nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='토큰 에러', msg_content='토큰 정보가 정확하지 않습니다.', data={}) + + except Exception as e: + logger.error(f"request error. URL: /room/score/ \nerror message: {e}") + return await response.error_res(auth_token=auth_token, auth_type='NOMAL', msg_title='로그인 에러', msg_content='로그인 처리중 에러가 발생했습니다.', data={}) + + + + + + +#================================================================================================== +# 필요한 함수 +#================================================================================================== +# 401 에러 발생 +def token_expired_return_process(fail_msg): + logger.error(f"request fail: {fail_msg}") + return HTTPException( + status_code=401, + detail=f"{fail_msg}" + ) + diff --git a/fastapi/app/router/router_api.py b/fastapi/app/router/router_api.py new file mode 100644 index 0000000..5e11034 --- /dev/null +++ b/fastapi/app/router/router_api.py @@ -0,0 +1,140 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from fastapi.security import APIKeyHeader +from typing import Union, Optional, List +from typing_extensions import Annotated +import json +import logging +import datetime +from kafka import KafkaProducer + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/api", + tags=["api", "collect"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- +# 비정상 요청 로그 기록 +logger = logging.getLogger() + +#2 logger의 level을 가장 낮은 수준인 DEBUG로 설정해둔다. +logger.setLevel(logging.ERROR) + +#3 formatter 지정 +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + +#4 handler instance 생성 +console = logging.StreamHandler() +file_handler = logging.FileHandler(filename="error_request.log") + +#5 handler 별로 다른 level 설정 +console.setLevel(logging.ERROR) +file_handler.setLevel(logging.ERROR) + +#6 handler 출력 format 지정 +console.setFormatter(formatter) +file_handler.setFormatter(formatter) + +#7 logger에 handler 추가 +logger.addHandler(console) +logger.addHandler(file_handler) + + +KAFKA_IP_DEV = "183.107.250.208:9092" +KAFKA_IP = "10.0.1.10:9092" +KAFKA_TOPIC = "boryeong-herit" + +api_key_header = APIKeyHeader(name="X-HIT-TransactionId") + +async def get_body(request: Request): + return await request.body() + + +async def get_header(header: str = Depends(api_key_header)): + return await header + + +# 헤더의 토큰 확인하는 +async def api_token(token: str = Depends(api_key_header)): + if len(token) == 0 or token.count('.') != 3: + logger.error("The value of 'header[X-HIT-TransactionId]' is not determined by a specific rule.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + token = token.split('.') + if len(token) != 4 or token[0] !='T' or len(token[3]) != 20: + logger.error("The value of 'header[X-HIT-TransactionId]' is not determined by a specific rule.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + +@router.post("/herit", dependencies=[Depends(api_token)]) +async def col_raw_data_from_herit( + content_Type: Optional[str] = Header(None), + X_HIT_TransactionId: Optional[str] = Header(None), + content_Length: Optional[str] = Header(None), + body: bytes = Depends(get_body) + ): + + data = { + 'content_Type': str(content_Type), + 'X_HIT_TransactionId': str(X_HIT_TransactionId), + 'content_Length': str(content_Length), + 'body': str(body.decode("UTF-8")) + } + message_producer = MessageProducer(KAFKA_IP, KAFKA_TOPIC) + # message_producer_dev = MessageProducer(KAFKA_IP_DEV, KAFKA_TOPIC) + + msg = { + "data_type": 'data_herit', + "data_herit": { + "content_type": data['content_Type'], + "x_hit_transactionid": data['X_HIT_TransactionId'], + "content_length": data['content_Length'], + "body": data['body'] + } + } + res = message_producer.send_message(msg) + # res_dev = message_producer_dev.send_message(msg) + + if res['status_code'] != status.HTTP_200_OK: + return {"reason": "SERVER ERROR"} + else : + return {"reason": "OK"} + + +# 테스트 URL +@router.post("/test") +async def col_raw_data_from_herit( + body: bytes = Depends(get_body) + ): + return { + "reason": "OK2", + "body": body + } + +# ----------------------------------------------------------------------------------------------- +# Kafka로 전달하기위한 클래스 +class MessageProducer: + broker = "" + topic = "" + producer = None + + def __init__(self, broker, topic): + self.broker = broker + self.topic = topic + self.producer = KafkaProducer(bootstrap_servers=self.broker, + value_serializer=lambda x: json.dumps(x).encode('utf-8'), + acks=0, + api_version=(2,5,0), + retries=3 + ) + + def send_message(self, msg): + try: + future = self.producer.send(self.topic, msg) + self.producer.flush() # 비우는 작업 + future.get(timeout=60) + return {'status_code': 200, 'error': None} + except Exception as e: + logging.error(f"kafka process error: {e}") + return e diff --git a/fastapi/app/router/test_service.py b/fastapi/app/router/test_service.py new file mode 100644 index 0000000..d365650 --- /dev/null +++ b/fastapi/app/router/test_service.py @@ -0,0 +1,132 @@ +from fastapi import APIRouter, Depends, HTTPException, Header, Body, status, Request +from sqlalchemy.orm import Session +import json +import datetime +import requests +import jwt +import time +from process.logger import logger + +from db.base import get_db +import pandas as pd + +from common.config import SECRET_KEY + +KST = datetime.timezone(datetime.timedelta(hours=9)) + +router = APIRouter( + prefix="/test", + tags=["test"], + responses={404: {"description": "Not found"}}, +) + +# ----------------------------------------------------------------------------------------------- + +async def get_body(request: Request): + return await request.body() + + + + +# user_seq로 토큰 생성기 +@router.post("/make/token/{user_seq}") +async def make_new_token(user_seq: str, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + user_token = create_jwt(user_seq=int(user_seq), period=3600) + return { + "result": "OK", + "data": { + "token": user_token + } + } + + except Exception as e: + return { + "result": "FAIL", + "msg": e + } + + +# user_seq로 만료된 토큰 생성기 +@router.post("/make/token/expired/{user_seq}") +async def make_new_token_expired(user_seq: str, body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + user_token = create_jwt(user_seq=int(user_seq), period=0) + return { + "result": "OK", + "data": { + "token": user_token + } + } + + except Exception as e: + return { + "result": "FAIL", + "msg": e + } + + +# 토큰 정보 확인 +@router.post("/check/token") +async def check_token_data(body: bytes = Depends(get_body), db: Session = Depends(get_db)): + try: + # body에서 ID 추출 + try: + body = json.loads(body) + except json.JSONDecodeError as e: + return logger.error(f"json.loads error: Invalid JSON format in request body\nerror message: {e}") + + token = body['token'] + token_decode = decode_jwt(token=token) + + return { + "result": "OK", + "data": { + "user_seq": token_decode['user_seq'], + "exp": change_time(token_decode['exp']), + "iat": change_time(token_decode['iat']), + } + } + + except Exception as e: + return { + "result": "FAIL", + "msg": e + } + + + + +def create_jwt(user_seq: int, period: int): + # 현재 시간 + now = int(time.time()) + + # JWT의 페이로드 (클레임) + payload = { + "user_seq": user_seq, + "exp": now + period, # 만료 시간 + "iat": now, # 토큰 발행 시간 + } + + # JWT 생성 (HS256 알고리즘 사용) + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + + return token + + +def decode_jwt(token: str): + try: + # 토큰 디코드 + decoded_payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + return decoded_payload + except jwt.ExpiredSignatureError: + return {"error": "Token has expired"} + except jwt.InvalidTokenError: + return {"error": "Invalid token"} + + +# 현재 시간 YYYY-MM-DD hh:mm:ss로 변환 함수 +def change_time(time_data): + dt_object = datetime.datetime.fromtimestamp(time_data) + formatted_time = dt_object.strftime('%Y-%m-%d %H:%M:%S') + return formatted_time \ No newline at end of file diff --git a/fastapi/app/server.crt b/fastapi/app/server.crt new file mode 100644 index 0000000..e69de29 diff --git a/fastapi/app/server.key b/fastapi/app/server.key new file mode 100644 index 0000000..e69de29 diff --git a/fastapi/create_image_fastapi.sh b/fastapi/create_image_fastapi.sh new file mode 100644 index 0000000..f92c941 --- /dev/null +++ b/fastapi/create_image_fastapi.sh @@ -0,0 +1,4 @@ + + +#!/bin/bash +sudo docker build -t fastapi:v1 -f ./Dockerfile-fastapi . diff --git a/fastapi/docker-compose-fastapi.yml b/fastapi/docker-compose-fastapi.yml new file mode 100644 index 0000000..bd7c04a --- /dev/null +++ b/fastapi/docker-compose-fastapi.yml @@ -0,0 +1,26 @@ +version: "3.8" + +services: + api: + image: fastapi:v1 + environment: + - TZ=Asia/Seoul # 한국 시간대 설정 + volumes: + - /home/ec2-user/eld/card_service/backend/fastapi/app:/app + command: > + uvicorn main:app --host 0.0.0.0 --port 8098 + expose: + - "8098" + + nginx: + image: nginx:latest + environment: + - TZ=Asia/Seoul # 한국 시간대 설정 + ports: + - "8097:8097" # 외부에서 8097 포트로 접근 + volumes: + - /home/ec2-user/eld/card_service/backend/fastapi/nginx.conf:/etc/nginx/nginx.conf + - /home/ec2-user/eld/card_service/backend/fastapi/eld.crt:/etc/nginx/ssl/eld.crt + - /home/ec2-user/eld/card_service/backend/fastapi/eld.key:/etc/nginx/ssl/eld.key + depends_on: + - api diff --git a/fastapi/eld.crt b/fastapi/eld.crt new file mode 100644 index 0000000..ad0681e --- /dev/null +++ b/fastapi/eld.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGCDCCBPCgAwIBAgIQWwEQbfqkdYEA35goU9JMADANBgkqhkiG9w0BAQsFADBM +MQswCQYDVQQGEwJMVjENMAsGA1UEBxMEUmlnYTERMA8GA1UEChMIR29HZXRTU0wx +GzAZBgNVBAMTEkdvR2V0U1NMIFJTQSBEViBDQTAeFw0yNDA3MjMwMDAwMDBaFw0y +NTA3MjMyMzU5NTlaMBYxFDASBgNVBAMTC2VsZHNvZnQuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkYA7IYc330ya/BFBhcG8QQrdKuV5GPV5rh0K +NIeCU0yWl5ydYzpXhWuFE/qnQfVRK5g+1jAfBK4UHTZdpdvWzdDlZFkJBiQO0MS2 +/ujWpTWSEYqalgRxZylTDSOG+KAt+Tpo0ZWv/T6okjW54+J2vi4E/2QGPzi/Dr5c +d9hlMBB3VREsmDqDScVnHOlxbMVLf/EgIEP5CbGwdV71/R5Tpcdkr9ubw5s1RdMz +GEYfQEJM56zdlAwhG7ucadoIVXRlP6BWmyrk9uZgcDg1waUoFekyCGg/gwhdzoH5 +xglDwb1tL/b+jxhgm8Cu0Av5GwaD45LSc1B1+AHM99qnoETuKQIDAQABo4IDGjCC +AxYwHwYDVR0jBBgwFoAU+ftQxItnu2dk/oMhpqnOP1WEk5kwHQYDVR0OBBYEFLxH +VZ+saqBmJr8jEwI3UhYLh4DLMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAA +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBLBgNVHSAERDBCMDYGCysG +AQQBsjEBAgJAMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vY3BzLnVzZXJ0cnVzdC5j +b20wCAYGZ4EMAQIBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwudXNlcnRy +dXN0LmNvbS9Hb0dldFNTTFJTQURWQ0EuY3JsMG8GCCsGAQUFBwEBBGMwYTA4Bggr +BgEFBQcwAoYsaHR0cDovL2NydC51c2VydHJ1c3QuY29tL0dvR2V0U1NMUlNBRFZD +QS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wFgYD +VR0RBA8wDYILZWxkc29mdC5jb20wggGABgorBgEEAdZ5AgQCBIIBcASCAWwBagB2 +AN3cyjSV1+EWBeeVMvrHn/g9HFDf2wA6FBJ2Ciysu8gqAAABkN5FaiIAAAQDAEcw +RQIgHTbwtmQN6xLMETdF+8eqQ7AKqXO8N2f+bD7Fx7tzlo4CIQDc3kmta7RofW+L +jZxQtI4p3b4hFUFCDw85ibQgCSpH6QB3AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw +7w5CHrR+Tqo0AAABkN5Fae8AAAQDAEgwRgIhAI2ZgaTuuAjcfrg6o2KeX+O8ga8e +tGY7+NZRt/yRXuyXAiEA5bEkR2LchqMV3vYr5Eo/7nKm8eTXNBhduxNIPxNzRoAA +dwAS8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZDeRWnNAAAEAwBI +MEYCIQDT4+jvqlZboMs6AeqxZRu0TyKB/zA5UzGfJdaCaWQ8iAIhAKgg7Q+W0sd/ +Gxaa2pHXpHSxP3sfkT9o5Br8S4pILca3MA0GCSqGSIb3DQEBCwUAA4IBAQA51hYf +mw+kueMcWmPUlUikrK0/KuLpSfWPvHOj+r84Y5AS9JxZmi9b+9W18p1VwO0YXR6U +ho4PyHjuIviT2LviAWyvRTFc9Il+e920+RyMiAdfQ/Af15xdcAJOjSMlZu3xkqvW +gkgE1kPvIQevqCmEEzUqsUVEn7ftoDcT9SEJBpLFwB6FAFBfw0pvJ0qUr9LAin4p +bklWGaY2laI2py7MXZNUO35rahA0DNS/Y2sY3TbH/WXilJ/sbJ9sO83/XC2T7vgC +Le2cv9gNFJPiTW++OROxybXzarwIwWFHO7aPc5Ehv9U54/XlzBIzrRsJuvtXkc0I +0nuiAfpjOom89Z1W +-----END CERTIFICATE----- \ No newline at end of file diff --git a/fastapi/eld.csr b/fastapi/eld.csr new file mode 100644 index 0000000..6bf795c --- /dev/null +++ b/fastapi/eld.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwbDEUMBIGA1UEAwwLZWxkc29mdC5jb20xHzAdBgNVBAoMFuyj +vOyLne2ajOyCrCDsnbTsl5jrlJQxEjAQBgNVBAgMCeuMgOyghOyLnDESMBAGA1UE +BwwJ7Jyg7ISx6rWsMQswCQYDVQQGEwJLUjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAJGAOyGHN99MmvwRQYXBvEEK3SrleRj1ea4dCjSHglNMlpecnWM6 +V4VrhRP6p0H1USuYPtYwHwSuFB02XaXb1s3Q5WRZCQYkDtDEtv7o1qU1khGKmpYE +cWcpUw0jhvigLfk6aNGVr/0+qJI1uePidr4uBP9kBj84vw6+XHfYZTAQd1URLJg6 +g0nFZxzpcWzFS3/xICBD+QmxsHVe9f0eU6XHZK/bm8ObNUXTMxhGH0BCTOes3ZQM +IRu7nGnaCFV0ZT+gVpsq5PbmYHA4NcGlKBXpMghoP4MIXc6B+cYJQ8G9bS/2/o8Y +YJvArtAL+RsGg+OS0nNQdfgBzPfap6BE7ikCAwEAATANBgkqhkiG9w0BAQsFAAOC +AQEAZhOyuc//t8Vp+Iog/C12fFSIB4baHBS/GV3C/xm1gvmn/Vl8ynaswPA3ei0U +rvoXidZHYt7gdcBq1DlPDGxbnEwNYZhNFR0Sv622EO9MrsiUsje6yWm2xmCgM8zw +ChhifmEulSBEFcOQGi9eKA19jJHKEqRx5QhlZHsRdVD+glxyESpOM0cfUXGsYXhn +Pvm9EvlZfYyoVBRjIwO3CIKSaGsgg2FKUKnmgMo28BKVPYM1/gNC47ozhwMOgDrO +fg1KZ3CtOMn3ZG7OiN2HamnBNKQDDp6mZJT1aoq1GsENYFNJUfW768G2rmAUGNfw +My3PBjidoiuChvimG1Fqpnioxw== +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/fastapi/eld.key b/fastapi/eld.key new file mode 100644 index 0000000..6c3f2d3 --- /dev/null +++ b/fastapi/eld.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAkYA7IYc330ya/BFBhcG8QQrdKuV5GPV5rh0KNIeCU0yWl5yd +YzpXhWuFE/qnQfVRK5g+1jAfBK4UHTZdpdvWzdDlZFkJBiQO0MS2/ujWpTWSEYqa +lgRxZylTDSOG+KAt+Tpo0ZWv/T6okjW54+J2vi4E/2QGPzi/Dr5cd9hlMBB3VREs +mDqDScVnHOlxbMVLf/EgIEP5CbGwdV71/R5Tpcdkr9ubw5s1RdMzGEYfQEJM56zd +lAwhG7ucadoIVXRlP6BWmyrk9uZgcDg1waUoFekyCGg/gwhdzoH5xglDwb1tL/b+ +jxhgm8Cu0Av5GwaD45LSc1B1+AHM99qnoETuKQIDAQABAoIBAAhxYU0CYoe4s2AX +1L5hFe5MxfVqpCaifOVxbjGK4PE6Nx1UU04qGSDG8s2MXIb/aA7AanoFiBE+lDB3 +QoMgsPPXsK3sXDGQ51KuLYO4aVckFwYhTbPRjW6L53OyWW9FJTHKdcFenxwR9hho +2XDrp9IEi9nxeQrTXUPK4ET8h6+cmHgEDEXJ18xyn3EMrXbBTOBaAGJwdA/kE947 +o85Yu+o3sKA+3JsuuMijSy2XSqvslNNeyOvlcFcq88eDY5pXuDGbRQPnIQe3t0Fc +DMtIF2E2jOtIbMuBdcdZaUduiPqVRT3ojW2LaDc58H7+piAgBUVGJCLuqIgujBa0 +t3n9M3ECgYEA1oe+oVG8+OVvcg9IJyQmnZOIR3hopuDvZOCYuSOsOS8DMo+p066z +6rSKpvtIcJJ8l0mM8Rw4WifEbEYOgkt9yGVK80mMI2BkSwmq5U4hOWg/xxqATTz5 +PBgz0bBwZ6ZgW/3RD+jWybX3vy+beifAL3v52q+hnZdZf1q1rBYYWXECgYEAraB/ +C/a4RO0LygrncUgn9MX4mGe2zRVAszZcbcX2iG4o1mseoZ16HO1ipS4Q/kI/znpY +r7XT4QgF98Fdkcl8MYKP+snUv9TG7zOFiYOe0JeaafBuukMDPUqeQXk/d9rSrJg9 +lqiw1LYYyxPnh8aTEfwMBPqaRhLiMeD63PsHRDkCgYEAzwflUi1dnx1b9ckFqrBa +i8tqwv5SkGmW3dVZzaG9fNn/zfWSwPRiMOjWvdrWx7y2fBHA8JZ5U5f5GTxqmBde +ZdxK/opFsYY+g6PqxqwlqA8RLYZHt0JWjEYXDA+oCn8nkt9ZuG7NiZAQbPL2qmZe +M/UC5KaF413CQwM5O79+9CECgYBT/D+YNOabiKJcP/wGAuY48441gm2dNDuQtKnu ++4QuKEMevMAbYwZPedBuoCLeKoOcx/egPu7Xej8QwfsV6wVlGYe1wu1jQXRc/moI +w58NvVeXCRM2i/XELxTwDMtTmYiwrg+UkdK/gbnqeZ1UQwye9XGG8wWvAbFieTY/ +sDmqmQKBgQCUyu98Gpatoe8MdyHZf9h+QfYcUkpLuhJOvWCGQt9QyfiIal5eleXG +YPNaQBk3GtSPyuB1nDrjtHaIlyw1ScjRCfzth3VJvwWnqz9y7XP2pow2+TnY0Jco +lvMRY9n55TgAPdTdgHshUyDRAM1/aUbiBIPPOP/XAcmONZ1lpayZ7A== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/fastapi/nginx.conf b/fastapi/nginx.conf new file mode 100644 index 0000000..bcadcdc --- /dev/null +++ b/fastapi/nginx.conf @@ -0,0 +1,22 @@ +events { + worker_connections 2048; +} + +http { + server { + listen 8097 ssl; + server_name eldsoft.com; + + ssl_certificate /etc/nginx/ssl/eld.crt; + ssl_certificate_key /etc/nginx/ssl/eld.key; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://api:8098; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } +} diff --git a/fastapi/restart.sh b/fastapi/restart.sh new file mode 100644 index 0000000..76fea9a --- /dev/null +++ b/fastapi/restart.sh @@ -0,0 +1,10 @@ + + +#!/bin/bash +docker-compose -f /home/ec2-user/eld/card_service/backend/fastapi/docker-compose-fastapi.yml down +docker-compose -f /home/ec2-user/eld/card_service/backend/fastapi/docker-compose-fastapi.yml up -d + +sleep 1 + +docker-compose -f /home/ec2-user/eld/card_service/backend/fastapi/docker-compose-fastapi.yml ps + diff --git a/kafka/Dockerfile-kafka b/kafka/Dockerfile-kafka new file mode 100644 index 0000000..f065647 --- /dev/null +++ b/kafka/Dockerfile-kafka @@ -0,0 +1,44 @@ +FROM azul/zulu-openjdk-alpine:8u292-8.54.0.21 + +ARG kafka_version=2.7.1 +ARG scala_version=2.13 +ARG glibc_version=2.31-r0 +ARG vcs_ref=unspecified +ARG build_date=unspecified + +LABEL org.label-schema.name="kafka" \ + org.label-schema.description="Apache Kafka" \ + org.label-schema.build-date="${build_date}" \ + org.label-schema.vcs-url="https://github.com/wurstmeister/kafka-docker" \ + org.label-schema.vcs-ref="${vcs_ref}" \ + org.label-schema.version="${scala_version}_${kafka_version}" \ + org.label-schema.schema-version="1.0" \ + maintainer="wurstmeister" + +ENV KAFKA_VERSION=$kafka_version \ + SCALA_VERSION=$scala_version \ + KAFKA_HOME=/opt/kafka \ + GLIBC_VERSION=$glibc_version + +ENV PATH=${PATH}:${KAFKA_HOME}/bin + +COPY ./kafka/download-kafka.sh ./kafka/start-kafka.sh ./kafka/broker-list.sh ./kafka/create-topics.sh ./kafka/versions.sh /tmp/ + +RUN apk add --no-cache bash curl jq docker \ + && chmod a+x /tmp/*.sh \ + && mv /tmp/start-kafka.sh /tmp/broker-list.sh /tmp/create-topics.sh /tmp/versions.sh /usr/bin \ + && sync && /tmp/download-kafka.sh \ + && tar xfz /tmp/kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz -C /opt \ + && rm /tmp/kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz \ + && ln -s /opt/kafka_${SCALA_VERSION}-${KAFKA_VERSION} ${KAFKA_HOME} \ + && rm /tmp/* \ + && wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk \ + && apk add --no-cache --allow-untrusted glibc-${GLIBC_VERSION}.apk \ + && rm glibc-${GLIBC_VERSION}.apk + +COPY ./kafka/overrides /opt/overrides + +VOLUME ["/kafka"] + +# Use "exec" form so that it runs as PID 1 (useful for graceful shutdown) +CMD ["start-kafka.sh"] \ No newline at end of file diff --git a/kafka/docker-compose-kafka.yml b/kafka/docker-compose-kafka.yml new file mode 100644 index 0000000..3f27c18 --- /dev/null +++ b/kafka/docker-compose-kafka.yml @@ -0,0 +1,18 @@ +version: '2' +services: + zookeeper: + image: wurstmeister/zookeeper + ports: + - "2181:2181" + kafka: + build: + context: . + dockerfile: ./Dockerfile-kafka + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_HOST_NAME: 10.0.1.20 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_CREATE_TOPICS: "cheonan-kt:1:1,cheonan-lg:1:1" + volumes: + - /var/run/docker_kafka.sock:/var/run/docker.sock diff --git a/kafka/kafka/broker-list.sh b/kafka/kafka/broker-list.sh new file mode 100644 index 0000000..5c5ee2d --- /dev/null +++ b/kafka/kafka/broker-list.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +CONTAINERS=$(docker ps | grep 9092 | awk '{print $1}') +BROKERS=$(for CONTAINER in ${CONTAINERS}; do docker port "$CONTAINER" 9092 | sed -e "s/0.0.0.0:/$HOST_IP:/g"; done) +echo "${BROKERS//$'\n'/,}" diff --git a/kafka/kafka/create-topics.sh b/kafka/kafka/create-topics.sh new file mode 100644 index 0000000..0bacf7b --- /dev/null +++ b/kafka/kafka/create-topics.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +if [[ -z "$KAFKA_CREATE_TOPICS" ]]; then + exit 0 +fi + +if [[ -z "$START_TIMEOUT" ]]; then + START_TIMEOUT=600 +fi + +start_timeout_exceeded=false +count=0 +step=10 +while netstat -lnt | awk '$4 ~ /:'"$KAFKA_PORT"'$/ {exit 1}'; do + echo "waiting for kafka to be ready" + sleep $step; + count=$((count + step)) + if [ $count -gt $START_TIMEOUT ]; then + start_timeout_exceeded=true + break + fi +done + +if $start_timeout_exceeded; then + echo "Not able to auto-create topic (waited for $START_TIMEOUT sec)" + exit 1 +fi + +# introduced in 0.10. In earlier versions, this will fail because the topic already exists. +# shellcheck disable=SC1091 +source "/usr/bin/versions.sh" +if [[ "$MAJOR_VERSION" == "0" && "$MINOR_VERSION" -gt "9" ]] || [[ "$MAJOR_VERSION" -gt "0" ]]; then + KAFKA_0_10_OPTS="--if-not-exists" +fi + +# Expected format: +# name:partitions:replicas:cleanup.policy +IFS="${KAFKA_CREATE_TOPICS_SEPARATOR-,}"; for topicToCreate in $KAFKA_CREATE_TOPICS; do + echo "creating topics: $topicToCreate" + IFS=':' read -r -a topicConfig <<< "$topicToCreate" + config= + if [ -n "${topicConfig[3]}" ]; then + config="--config=cleanup.policy=${topicConfig[3]}" + fi + + COMMAND="JMX_PORT='' ${KAFKA_HOME}/bin/kafka-topics.sh \\ + --create \\ + --zookeeper ${KAFKA_ZOOKEEPER_CONNECT} \\ + --topic ${topicConfig[0]} \\ + --partitions ${topicConfig[1]} \\ + --replication-factor ${topicConfig[2]} \\ + ${config} \\ + ${KAFKA_0_10_OPTS} &" + eval "${COMMAND}" +done + +wait diff --git a/kafka/kafka/download-kafka.sh b/kafka/kafka/download-kafka.sh new file mode 100644 index 0000000..746ef93 --- /dev/null +++ b/kafka/kafka/download-kafka.sh @@ -0,0 +1,18 @@ +#!/bin/sh -e + +# shellcheck disable=SC1091 +source "/usr/bin/versions.sh" + +FILENAME="kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz" + +url=$(curl --stderr /dev/null "https://www.apache.org/dyn/closer.cgi?path=/kafka/${KAFKA_VERSION}/${FILENAME}&as_json=1" | jq -r '"\(.preferred)\(.path_info)"') + +# Test to see if the suggested mirror has this version, currently pre 2.1.1 versions +# do not appear to be actively mirrored. This may also be useful if closer.cgi is down. +if [[ ! $(curl -f -s -r 0-1 "${url}") ]]; then + echo "Mirror does not have desired version, downloading direct from Apache" + url="https://archive.apache.org/dist/kafka/${KAFKA_VERSION}/${FILENAME}" +fi + +echo "Downloading Kafka from $url" +wget "${url}" -O "/tmp/${FILENAME}" diff --git a/kafka/kafka/overrides/0.9.0.1.sh b/kafka/kafka/overrides/0.9.0.1.sh new file mode 100644 index 0000000..d5e8561 --- /dev/null +++ b/kafka/kafka/overrides/0.9.0.1.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +# Kafka 0.9.x.x has a 'listeners' config by default. We need to remove this +# as the user may be configuring via the host.name / advertised.host.name properties +echo "Removing 'listeners' from server.properties pre-bootstrap" +sed -i -e '/^listeners=/d' "$KAFKA_HOME/config/server.properties" diff --git a/kafka/kafka/start-kafka.sh b/kafka/kafka/start-kafka.sh new file mode 100644 index 0000000..b48280a --- /dev/null +++ b/kafka/kafka/start-kafka.sh @@ -0,0 +1,149 @@ +#!/bin/bash -e + +# Allow specific kafka versions to perform any unique bootstrap operations +OVERRIDE_FILE="/opt/overrides/${KAFKA_VERSION}.sh" +if [[ -x "$OVERRIDE_FILE" ]]; then + echo "Executing override file $OVERRIDE_FILE" + eval "$OVERRIDE_FILE" +fi + +# Store original IFS config, so we can restore it at various stages +ORIG_IFS=$IFS + +if [[ -z "$KAFKA_ZOOKEEPER_CONNECT" ]]; then + echo "ERROR: missing mandatory config: KAFKA_ZOOKEEPER_CONNECT" + exit 1 +fi + +if [[ -z "$KAFKA_PORT" ]]; then + export KAFKA_PORT=9092 +fi + +create-topics.sh & +unset KAFKA_CREATE_TOPICS + +if [[ -z "$KAFKA_ADVERTISED_PORT" && \ + -z "$KAFKA_LISTENERS" && \ + -z "$KAFKA_ADVERTISED_LISTENERS" && \ + -S /var/run/docker.sock ]]; then + KAFKA_ADVERTISED_PORT=$(docker port "$(hostname)" $KAFKA_PORT | sed -r 's/.*:(.*)/\1/g' | head -n1) + export KAFKA_ADVERTISED_PORT +fi + +if [[ -z "$KAFKA_BROKER_ID" ]]; then + if [[ -n "$BROKER_ID_COMMAND" ]]; then + KAFKA_BROKER_ID=$(eval "$BROKER_ID_COMMAND") + export KAFKA_BROKER_ID + else + # By default auto allocate broker ID + export KAFKA_BROKER_ID=-1 + fi +fi + +if [[ -z "$KAFKA_LOG_DIRS" ]]; then + export KAFKA_LOG_DIRS="/kafka/kafka-logs-$HOSTNAME" +fi + +if [[ -n "$KAFKA_HEAP_OPTS" ]]; then + sed -r -i 's/(export KAFKA_HEAP_OPTS)="(.*)"/\1="'"$KAFKA_HEAP_OPTS"'"/g' "$KAFKA_HOME/bin/kafka-server-start.sh" + unset KAFKA_HEAP_OPTS +fi + +if [[ -n "$HOSTNAME_COMMAND" ]]; then + HOSTNAME_VALUE=$(eval "$HOSTNAME_COMMAND") + + # Replace any occurrences of _{HOSTNAME_COMMAND} with the value + IFS=$'\n' + for VAR in $(env); do + if [[ $VAR =~ ^KAFKA_ && "$VAR" =~ "_{HOSTNAME_COMMAND}" ]]; then + eval "export ${VAR//_\{HOSTNAME_COMMAND\}/$HOSTNAME_VALUE}" + fi + done + IFS=$ORIG_IFS +fi + +if [[ -n "$PORT_COMMAND" ]]; then + PORT_VALUE=$(eval "$PORT_COMMAND") + + # Replace any occurrences of _{PORT_COMMAND} with the value + IFS=$'\n' + for VAR in $(env); do + if [[ $VAR =~ ^KAFKA_ && "$VAR" =~ "_{PORT_COMMAND}" ]]; then + eval "export ${VAR//_\{PORT_COMMAND\}/$PORT_VALUE}" + fi + done + IFS=$ORIG_IFS +fi + +if [[ -n "$RACK_COMMAND" && -z "$KAFKA_BROKER_RACK" ]]; then + KAFKA_BROKER_RACK=$(eval "$RACK_COMMAND") + export KAFKA_BROKER_RACK +fi + +# Try and configure minimal settings or exit with error if there isn't enough information +if [[ -z "$KAFKA_ADVERTISED_HOST_NAME$KAFKA_LISTENERS" ]]; then + if [[ -n "$KAFKA_ADVERTISED_LISTENERS" ]]; then + echo "ERROR: Missing environment variable KAFKA_LISTENERS. Must be specified when using KAFKA_ADVERTISED_LISTENERS" + exit 1 + elif [[ -z "$HOSTNAME_VALUE" ]]; then + echo "ERROR: No listener or advertised hostname configuration provided in environment." + echo " Please define KAFKA_LISTENERS / (deprecated) KAFKA_ADVERTISED_HOST_NAME" + exit 1 + fi + + # Maintain existing behaviour + # If HOSTNAME_COMMAND is provided, set that to the advertised.host.name value if listeners are not defined. + export KAFKA_ADVERTISED_HOST_NAME="$HOSTNAME_VALUE" +fi + +#Issue newline to config file in case there is not one already +echo "" >> "$KAFKA_HOME/config/server.properties" + +( + function updateConfig() { + key=$1 + value=$2 + file=$3 + + # Omit $value here, in case there is sensitive information + echo "[Configuring] '$key' in '$file'" + + # If config exists in file, replace it. Otherwise, append to file. + if grep -E -q "^#?$key=" "$file"; then + sed -r -i "s@^#?$key=.*@$key=$value@g" "$file" #note that no config values may contain an '@' char + else + echo "$key=$value" >> "$file" + fi + } + + # Fixes #312 + # KAFKA_VERSION + KAFKA_HOME + grep -rohe KAFKA[A-Z0-0_]* /opt/kafka/bin | sort | uniq | tr '\n' '|' + EXCLUSIONS="|KAFKA_VERSION|KAFKA_HOME|KAFKA_DEBUG|KAFKA_GC_LOG_OPTS|KAFKA_HEAP_OPTS|KAFKA_JMX_OPTS|KAFKA_JVM_PERFORMANCE_OPTS|KAFKA_LOG|KAFKA_OPTS|" + + # Read in env as a new-line separated array. This handles the case of env variables have spaces and/or carriage returns. See #313 + IFS=$'\n' + for VAR in $(env) + do + env_var=$(echo "$VAR" | cut -d= -f1) + if [[ "$EXCLUSIONS" = *"|$env_var|"* ]]; then + echo "Excluding $env_var from broker config" + continue + fi + + if [[ $env_var =~ ^KAFKA_ ]]; then + kafka_name=$(echo "$env_var" | cut -d_ -f2- | tr '[:upper:]' '[:lower:]' | tr _ .) + updateConfig "$kafka_name" "${!env_var}" "$KAFKA_HOME/config/server.properties" + fi + + if [[ $env_var =~ ^LOG4J_ ]]; then + log4j_name=$(echo "$env_var" | tr '[:upper:]' '[:lower:]' | tr _ .) + updateConfig "$log4j_name" "${!env_var}" "$KAFKA_HOME/config/log4j.properties" + fi + done +) + +if [[ -n "$CUSTOM_INIT_SCRIPT" ]] ; then + eval "$CUSTOM_INIT_SCRIPT" +fi + +exec "$KAFKA_HOME/bin/kafka-server-start.sh" "$KAFKA_HOME/config/server.properties" diff --git a/kafka/kafka/versions.sh b/kafka/kafka/versions.sh new file mode 100644 index 0000000..d790d1a --- /dev/null +++ b/kafka/kafka/versions.sh @@ -0,0 +1,7 @@ +#!/bin/bash -e + +MAJOR_VERSION=$(echo "$KAFKA_VERSION" | cut -d. -f1) +export MAJOR_VERSION + +MINOR_VERSION=$(echo "$KAFKA_VERSION" | cut -d. -f2) +export MINOR_VERSION diff --git a/kafka/restart-kafka.sh b/kafka/restart-kafka.sh new file mode 100644 index 0000000..a62aa46 --- /dev/null +++ b/kafka/restart-kafka.sh @@ -0,0 +1,12 @@ + + + + +#!/bin/bash +docker-compose -f /data/cheonan_ep/docker/col_server/kafka/docker-compose-kafka.yml down +docker-compose -f /data/cheonan_ep/docker/col_server/kafka/docker-compose-kafka.yml up -d + +sleep 1 + +docker-compose -f /data/cheonan_ep/docker/col_server/kafka/docker-compose-kafka.yml ps + diff --git a/logstash/Dockerfile-logstash b/logstash/Dockerfile-logstash new file mode 100644 index 0000000..78270a8 --- /dev/null +++ b/logstash/Dockerfile-logstash @@ -0,0 +1,10 @@ + +FROM docker.elastic.co/logstash/logstash-oss:7.15.2 + +ENV TZ=Asia/Seoul + +COPY lib_logstash/postgresql-42.2.24.jar /usr/share/logstash/postgresql-42.2.24.jar + +RUN rm -f /usr/share/logstash/pipeline/logstash_kt.conf && \ + rm -f /usr/share/logstash/pipeline/logstash_lg.conf && \ + bin/logstash-plugin install logstash-output-jdbc diff --git a/logstash/conf/logstash_iot/logstash_kt.conf b/logstash/conf/logstash_iot/logstash_kt.conf new file mode 100644 index 0000000..e453746 --- /dev/null +++ b/logstash/conf/logstash_iot/logstash_kt.conf @@ -0,0 +1,49 @@ + +input { + kafka { + bootstrap_servers => "10.0.1.10:9092" + group_id => "cheonan" + topics => ["cheonan-kt"] + codec => json { + charset=>"UTF-8" + } + } +} + +# filter { +# mutate { +# split => {"message" => " "} +# } +# } + +output { +# stdout { +# codec => rubydebug +# } +# stdout { codec => line { format => 'tttttttttttttttttttttttt' } } + if [data_type] == "cheonan_kt" { + jdbc { + connection_string => "jdbc:postgresql://pg-219s5.vpc-cdb-kr.gov-ntruss.com:5432/cheonan_ep?user=cheonan&password=cjsdkstl2023!#" + driver_jar_path => "/usr/share/logstash/postgresql-42.2.24.jar" + max_pool_size => 2 + connection_timeout => 25000 + statement => [ + "INSERT INTO energy.raw_data_kt ( + tm, + request_type, + client_host, + body + ) VALUES ( + now(), + ?, + ?, + ? + )", + "[cheonan_kt][request_type]", + "[cheonan_kt][client_host]", + "[cheonan_kt][body]" + ] + } + } +} + diff --git a/logstash/conf/logstash_iot/logstash_lg.conf b/logstash/conf/logstash_iot/logstash_lg.conf new file mode 100644 index 0000000..f8c628c --- /dev/null +++ b/logstash/conf/logstash_iot/logstash_lg.conf @@ -0,0 +1,49 @@ + +input { + kafka { + bootstrap_servers => "10.0.1.10:9092" + group_id => "cheonan" + topics => ["cheonan-lg"] + codec => json { + charset=>"UTF-8" + } + } +} + +# filter { +# mutate { +# split => {"message" => " "} +# } +# } + +output { +# stdout { +# codec => rubydebug +# } +# stdout { codec => line { format => 'tttttttttttttttttttttttt' } } + if [data_type] == "cheonan_lg" { + jdbc { + connection_string => "jdbc:postgresql://pg-219s5.vpc-cdb-kr.gov-ntruss.com:5432/cheonan_ep?user=cheonan&password=cjsdkstl2023!#" + driver_jar_path => "/usr/share/logstash/postgresql-42.2.24.jar" + max_pool_size => 2 + connection_timeout => 25000 + statement => [ + "INSERT INTO energy.raw_data_lg ( + tm, + request_type, + client_host, + body + ) VALUES ( + now(), + ?, + ?, + ? + )", + "[cheonan_lg][request_type]", + "[cheonan_lg][client_host]", + "[cheonan_lg][body]" + ] + } + } +} + diff --git a/logstash/docker-compose-logstash.yml b/logstash/docker-compose-logstash.yml new file mode 100644 index 0000000..b2ff889 --- /dev/null +++ b/logstash/docker-compose-logstash.yml @@ -0,0 +1,15 @@ +version: '3' +services: + logstash-herit: + container_name: logstash + build: + context: . + dockerfile: ./Dockerfile-logstash + image: logstash:latest + volumes: + - /data/cheonan_ep/docker/col_server/logstash/conf/logstash_iot:/usr/share/logstash/pipeline + logging: + driver: "json-file" + options: + max-file: "5" + max-size: "100m" diff --git a/logstash/lib_logstash/postgresql-42.2.12.jar b/logstash/lib_logstash/postgresql-42.2.12.jar new file mode 100644 index 0000000..1f393bb Binary files /dev/null and b/logstash/lib_logstash/postgresql-42.2.12.jar differ diff --git a/logstash/lib_logstash/postgresql-42.2.24.jar b/logstash/lib_logstash/postgresql-42.2.24.jar new file mode 100644 index 0000000..02acc5f Binary files /dev/null and b/logstash/lib_logstash/postgresql-42.2.24.jar differ diff --git a/logstash/restart-logstash.sh b/logstash/restart-logstash.sh new file mode 100644 index 0000000..4570912 --- /dev/null +++ b/logstash/restart-logstash.sh @@ -0,0 +1,12 @@ + + + + +#!/bin/bash +docker-compose -f /data/cheonan_ep/docker/col_server/logstash/docker-compose-logstash.yml down +docker-compose -f /data/cheonan_ep/docker/col_server/logstash/docker-compose-logstash.yml up -d + +sleep 1 + +docker-compose -f /data/cheonan_ep/docker/col_server/logstash/docker-compose-logstash.yml ps + diff --git a/organize_image_files/organize_image_files.py b/organize_image_files/organize_image_files.py new file mode 100644 index 0000000..2d81a19 --- /dev/null +++ b/organize_image_files/organize_image_files.py @@ -0,0 +1,234 @@ +import logging +import sys +from logging.handlers import TimedRotatingFileHandler +import os +import stat +import shutil +import datetime +import psycopg2 # PostgreSQL 데이터베이스 연결 라이브러리 +import re + +# Custom handler class definition +class CustomTimedRotatingFileHandler(TimedRotatingFileHandler): + def __init__(self, base_log_path, when="midnight", interval=1, backupCount=7): + self.base_log_path = base_log_path + # Initialize log file path explicitly during instantiation + self.update_log_path() + super().__init__(self.baseFilename, when=when, interval=interval, backupCount=backupCount) + + def update_log_path(self): + current_time = datetime.datetime.now() + log_directory = os.path.join( + self.base_log_path, + current_time.strftime("%Y"), + current_time.strftime("%m") + ) + log_filename = current_time.strftime("%d_allscore.log") + + # Set log file path + self.baseFilename = os.path.join(log_directory, log_filename) + + # Create directory if not exists + if not os.path.exists(log_directory): + os.makedirs(log_directory) + + def emit(self, record): + # Update the log path at each log entry point + self.update_log_path() + super().emit(record) + +# Logger configuration +logger = logging.getLogger("allscore_logger") +logger.setLevel(logging.DEBUG) + +# Stream handler for stdout +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(logging.DEBUG) + +# Log format configuration +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +stream_handler.setFormatter(formatter) + +# Custom file handler configuration +base_log_path = "/home/ec2-user/eld/card_service/backend/organize_image_files/logs" +file_handler = CustomTimedRotatingFileHandler(base_log_path=base_log_path, when="midnight", interval=1, backupCount=7) +file_handler.setLevel(logging.DEBUG) +file_handler.setFormatter(formatter) + +# Add handlers to the logger +logger.addHandler(stream_handler) +logger.addHandler(file_handler) + + +# 파일명을 갖고 뉴스룸 SEQ찾기 +def get_user_seq(file_name): + try: + # PostgreSQL connection configuration + conn = psycopg2.connect( + dbname='allscore', + user='eldsoft', + password='eld240510', + host='3.34.123.25', # e.g., 'localhost' or IP address + port='8088' # e.g., '5432' + ) + + cur = conn.cursor() + + # Execute SQL query + query = f""" + select user_seq + from manage_user + where profile_img like '%{file_name}%' + """ + cur.execute(query) + result = cur.fetchall() + + # Close the cursor and the connection + cur.close() + conn.close() + + return result + + except Exception as e: + logger.error(f"get_user_seq(파일명을 갖고 user_seq 찾기) error: {e}") + + +# 서버 임시 디렉토리에서 하루전 파일들 목록 가져오기 +def get_file_name(): + try: + directory_path = f"/home/ec2-user/eld/card_service/backend/fastapi/app/images/user/temp_dir/profile_img/" + file_list = os.listdir(directory_path) + file_list = [f for f in file_list if os.path.isfile(os.path.join(directory_path, f))] + target_file_list = [] + for file_name in file_list: + if extract_datetime_from_filename(file_name): + target_file_list.append(file_name) + return file_list + + except Exception as e: + logger.error(f"임시 이미지 디렉토리에서 이미지 파일 가져오기 실패\nerror msg: {e}") + + +def extract_datetime_from_filename(filename): + # 정규 표현식으로 파일명에서 날짜 부분 추출 (확장자는 .jpg, .jpeg, .png, .bmp만 허용) + match = re.search(r'IMG_\d+_(\d+)\.(jpg|jpeg|png|bmp)', filename, re.IGNORECASE) + target_dt = match.group(1) # 추출된 날짜 부분 반환 + reference_date = get_yesterday_time_str() + if target_dt < reference_date: + return True + else: + return False + + +# 전날 00시 이전 파일명 갖고 오기위한 시간 str얻는 함수 +def get_yesterday_time_str(): + # 어제 시간 가져오기 + now = datetime.datetime.now() - datetime.timedelta(days=1) + # YYYYMMDDHHMMSSsss 형식으로 포맷 + formatted_time = now.strftime('%Y%m%d%H%M%S%f')[:17] + return formatted_time[:8] + '000000000' + + +# 파일 이동 +def move_file(user_seq, file_name): + try: + source_path = f"/home/ec2-user/eld/card_service/backend/fastapi/app/images/user/temp_dir/profile_img/{file_name}" + destination_dir = f"/home/ec2-user/eld/card_service/backend/fastapi/app/images/user/{user_seq}/profile_img/" + + # 대상 디렉토리가 존재하지 않으면 생성 + if not os.path.exists(destination_dir): + os.makedirs(destination_dir, mode=0o775) + + # 파일 쓰기 권한 부여 + os.chmod(source_path, stat.S_IWUSR) + + # 파일 이동 + shutil.move(source_path, destination_dir) + logger.debug(f"파일이 성공적으로 {destination_dir}로 이동되었습니다.") + + except Exception as e: + logger.error(f"파일 이동 중 오류가 발생했습니다: {e}") + + +# 파일 삭제 +def delete_file(file_name): + try: + file_path = f"/home/ec2-user/eld/card_service/backend/fastapi/app/images/user/temp_dir/profile_img/{file_name}" + # 파일이 존재하는지 확인 후 삭제 + if os.path.exists(file_path): + # 파일 쓰기 권한 부여 + os.chmod(file_path, stat.S_IWUSR) + # 파일 삭제 + os.remove(file_path) + logger.debug(f"{file_path} 파일이 성공적으로 삭제되었습니다.") + else: + logger.debug(f"{file_path} 파일이 존재하지 않습니다.") + except Exception as e: + logger.debug(f"파일 삭제 중 오류가 발생했습니다: {e}") + + +# 이동된 파일명 맞게 DB 수정하기 +def update_new_dir(user_seq, file_name): + try: + # PostgreSQL connection configuration + conn = psycopg2.connect( + dbname='allscore', + user='eldsoft', + password='eld240510', + host='3.34.123.25', # e.g., 'localhost' or IP address + port='8088' # e.g., '5432' + ) + + cur = conn.cursor() + logger.info("Connected to the database successfully.") + + source_path = f"/user/temp_dir/profile_img/{file_name}" + destination_dir = f"/user/{user_seq}/profile_img/{file_name}" + + # Execute SQL query + query = f""" + update manage_user + set + profile_img = '{destination_dir}' + where user_seq = {user_seq} + """ + logger.debug(f"query: {query}") + cur.execute(query) + + # Commit the transaction + conn.commit() + + # Close the cursor and the connection + cur.close() + conn.close() + + except Exception as e: + logger.error(f"update_new_dir(이동된 파일명 맞게 DB 수정하기) error: {e}") + + +# 회원 프로필이미지 임시 이미지 파일 정리 수행 +def manage_profile_img(): + # 임시 디렉토리에서 어제보다 전에 만들어진 파일명을 다 가져오기 + target_file_name = get_file_name() + logger.info(f"대상 파일 개수: {len(target_file_name)}개") + # 각 파일명별로 for문 수행 + for file_name in target_file_name: + # 해당 파일명이 포함된 게시글의 seq찾기 + user_seq = get_user_seq(file_name) + user_seq = '0' if len(user_seq) == 0 else user_seq[0][0] + logger.info(f"seq: {user_seq}") + # 해당 seq에 맞게 파일을 이동시키기, seq가 0인건(DB조회 되지 않은건 0으로 처리했기에) 삭제 + if user_seq == '0': + # seq가 0인건(DB조회 되지 않은건 0으로 처리했기에) 삭제 + delete_file(user_seq, file_name) + else: + # 해당 seq에 맞게 파일을 이동시키기 + move_file(user_seq, file_name) + # 이동시킨 디렉토리명에 맞춰서 DB내용(src)도 수정해주기 + update_new_dir(user_seq, file_name) + + +# Main execution +if __name__ == "__main__": + manage_profile_img() +