Django - Mini Project - 정리

김기훈·2025년 12월 2일

부트캠프 프로젝트

목록 보기
12/39

day 1 😕

디렉토리별 간단 설명 / 파일 상세 설명 / 필터

swagger 설정(drf-yasg)


django-is-awsome/
│
├── accounts/
│   ├── migrations/
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
│
├── transactions/
│   ├── migrations/
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
│
├── core/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│
├── docker/ (Docker 관련 설정 파일들이 위치)
│
├── .gitignore
├── .python-version
├── db.sqlite3
├── manage.py
├── poetry.lock
├── pyproject.toml
└── README.md

🗂️

core

  • core/settings.py

    • Django 프로젝트 전체 설정 파일 (앱 등록, 데이터베이스, REST Framework 설정 포함)
  • core/urls.py

    • 프로젝트의 최상위 URL 매핑
      • accounts/ , transactions/ 앱 URL을 include 해서 연결
  • core/wsgi.py

    • WSGI 서버 실행 진입점 (배포용)
  • core/asgi.py

    • ASGI 서버 실행 진입점 (웹소켓/비동기 지원)

accounts 앱

  • accounts/models.py

    • 사용자의 은행 계좌를 저장하는 모델.
      • User(FK) / 은행명 / 계좌번호 / 잔액 / 생성일자
  • accounts/serializers.py

    • Account 모델을 JSON으로 변환하는 Serializer.
    • fields → id, bank_name, account_number, balance.
  • accounts/views.py

    • 은행 계좌 CRUD API
      • AccountListCreateView → 로그인 유저의 계좌 목록 조회 & 생성
      • AccountDetailView → 한 계좌 조회·수정·삭제
    • 유저 본인의 계좌만 접근하도록 제한하는 로직 포함
  • accounts/urls.py

    • /accounts/ 경로에 대해:
      • GET/POST → 계좌 리스트/생성
      • GET/PUT/DELETE → 상세 조회·수정·삭제

transactions 앱

  • transactions/models.py

    • 거래 내역(Transaction) 모델:
      • 계좌(FK) / 유저(FK) / 금액 (+입금 / –출금) / 카테고리(식비, 교통 등)
      • 메모 / 거래 시각 / 생성/수정 시간 / 정렬 기준: 최신 거래가 먼저
  • transactions/serializers.py

    • Transaction 모델 변환 Serializer
    • read_only: user, created_at, updated_at
      • 개인적으로 수정 불가
  • transactions/views.py

    • 거래 CRUD API:
    • TransactionListCreateView
      • 로그인 유저의 거래만 조회
      • 필터링: category, 날짜(start/end)
      • ordering: 거래 시간, 금액
      • 생성 시 계좌의 소유자 확인 (권한 체크)
    • TransactionDetailView
      • 특정 거래 조회·수정·삭제
  • transactions/urls.py

    • /transactions/ 경로 처리:
      • GET/POST → 거래 목록·생성
      • <pk>/ → 단건 조회·수정·삭제

🗂️ 상세 설명

accounts

urls.py

from django.urls import path  
from .views import AccountListCreateView, AccountDetailView  

app_name = 'accounts'  

urlpatterns = [
    path('', AccountListCreateView.as_view(), name='account_list'),
    path('<int:pk>/', AccountDetailView.as_view(), name='account_detail'), 
]
  • app_name = 'accounts' → URL 네임스페이스 설정
    • 나중에 html에서 {% url app_name:name %} 처럼 사용
  • path('', AccountListCreateView.as_view(), name='account_list')
    • /accounts/ → 계좌 목록 조회 & 생성
  • path('<int:pk>/', AccountDetailView.as_view(), name='account_detail')
    • /accounts/<pk>/ → 계좌 상세 조회/수정/삭제

views.py

from rest_framework.permissions import IsAuthenticated  
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView  
from .models import Account  
from .serializers import AccountSerializer  

class AccountListCreateView(ListCreateAPIView):
    serializer_class = AccountSerializer  
    permission_classes = [IsAuthenticated]  

    def get_queryset(self):
        return Account.objects.filter(user=self.request.user)  # 현재 로그인 유저의 계좌만 반환

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)  # 계좌 생성 시 자동으로 user 연결
  • ListCreateAPIView → 계좌 목록 조회 + 생성 기능 제공

  • permission_classes = [IsAuthenticated] → 인증 필요

  • serializer_class = AccountSerializer → Serializer 지정

    • AccountSerializer → Account 데이터를 직렬화하는 Serializer
class AccountDetailView(RetrieveUpdateDestroyAPIView):  
    serializer_class = AccountSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Account.objects.filter(user=self.request.user)  # 본인의 계좌만 접근 허용
  • RetrieveUpdateDestroyAPIView → 단일 계좌 조회·수정·삭제


serializers.py

from rest_framework import serializers  
from .models import Account 

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account  
        fields = ['id', 'bank_name', 'account_number', 'balance']  
  • serializers.ModelSerializer → 모델 기반 Serializer 정의

  • fields → 응답/요청에 포함할 필드


models.py

from django.db import models  # Django 모델 기능 import
from django.contrib.auth import get_user_model  
User = get_user_model()  
class Account(models.Model): 
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='accounts')
    bank_name = models.CharField(max_length=50) 
    account_number = models.CharField(max_length=30, unique=True)
    balance = models.IntegerField(default=0)  # 잔액 (기본값 0)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user.username} - {self.bank_name}"  

transactions

views.py

from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from rest_framework.permissions import IsAuthenticated
from .models import Transaction
from .serializers import TransactionSerializer
from django_filters.rest_framework import DjangoFilterBackend  
from rest_framework.filters import OrderingFilter  
from rest_framework.exceptions import PermissionDenied  
  • DjangoFilterBackend / OrderingFilter / PermissionDenied

    • 필터 기능 제공 / 정렬 기능 제공 / 권한 오류 응답

class TransactionListCreateView(ListCreateAPIView): 
    serializer_class = TransactionSerializer
    permission_classes = [IsAuthenticated]

    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_fields = ['category']  
    ordering_fields = ['transacted_at', 'amount']  
  • ListCreateAPIView → 거래 조회 + 생성 API
  • filter_backends = [DjangoFilterBackend, OrderingFilter] → 필터/정렬 백엔드 적용
  • filterset_fields = ['category'] → /?category=food 필터
  • ordering_fields = ['transacted_at', 'amount'] → 정렬 필드 허용
    def get_queryset(self):
        user = self.request.user  # 현재 로그인 유저
        qs = Transaction.objects.filter(user=user)  # 본인 거래만 조회

        start = self.request.query_params.get("start")  # 날짜 필터: 시작
        end = self.request.query_params.get("end")  # 날짜 필터: 끝

        if start:
            qs = qs.filter(transacted_at__gte=start)  # 지정한 날짜 이후
        if end:
            qs = qs.filter(transacted_at__lte=end)  # 지정한 날짜 이전

        return qs
    def perform_create(self, serializer):
        account = serializer.validated_data['account']  # body에서 account 필드 추출
        if account.user != self.request.user:  # 본인 계좌인지 확인
            raise PermissionDenied("이 계좌에 거래를 생성할 수 없습니다.")  # 아니면 오류 발생
        serializer.save(user=self.request.user)  # 거래 생성 시 user 자동 등록
class TransactionDetailView(RetrieveUpdateDestroyAPIView):
    serializer_class = TransactionSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Transaction.objects.filter(user=self.request.user)  # 본인 거래만 접근 가능
  • RetrieveUpdateDestroyAPIView → 단일 거래 조회·수정·삭제

urls.py

from django.urls import path  
from .views import TransactionListCreateView, TransactionDetailView 

app_name = 'transactions' 

urlpatterns = [
    path('', TransactionListCreateView.as_view(), name='transaction_list'),
    path('<int:pk>/', TransactionDetailView.as_view(), name='transaction_detail'),  
]
  • /transactions/ | /transactions/<pk>/


serializers.py

from rest_framework import serializers  
from .models import Transaction  

class TransactionSerializer(serializers.ModelSerializer):  
    class Meta:
        model = Transaction  
        fields = [
            'id', 'account','user' ,'amount', 'category', 'memo',
            'transacted_at', 'created_at', 'updated_at'
        ]
        read_only_fields = ['user', 'created_at', 'updated_at']
  • read_only_fields → 수정 불가 필드


models.py

from django.db import models   
from django.contrib.auth import get_user_model
from accounts.models import Account 

User = get_user_model()

class Transaction(models.Model):
    CATEGORY_CHOICES = [ 
        ('food', '식비'),
        ('transport', '교통'),
        ('shopping', '쇼핑'),
        ('income', '수입'),
        ('etc', '기타'),
    ]
    account = models.ForeignKey(Account, 
    on_delete=models.CASCADE, related_name="transactions") # 거래된 계좌
    
    user = models.ForeignKey(User, on_delete=models.CASCADE)  # 거래 주인 (로그인 유저)

    amount = models.IntegerField()  # 금액 (+입금 / -출금)
    category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) 
    memo = models.CharField(max_length=255, blank=True)  # 메모 (선택)

    transacted_at = models.DateTimeField()  # 실제 거래 시각

    created_at = models.DateTimeField(auto_now_add=True)  # 생성 시각 자동 기록
    updated_at = models.DateTimeField(auto_now=True)  # 수정 시각 자동 기록

    class Meta:
        ordering = ["-transacted_at"]  # 최신 거래 먼저

    def __str__(self):
        return f"{self.account} / {self.amount}" 

core

urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),  # /admin/ 관리자 페이지
    path("accounts/", include('accounts.urls')),  # accounts 앱 URL 연결
    path("transactions/", include('transactions.urls')),  # transactions 앱 URL 연결
]

테스트 📝

필수 ❗️

# settings.py 
'DEFAULT_AUTHENTICATION_CLASSES': [
    'rest_framework.authentication.SessionAuthentication',
    'rest_framework.authentication.BasicAuthentication'
]

# Views.py 
permission_classes = [IsAuthenticated]
  • 반드시 로그인(세션 인증) 필요


계좌 (accounts)

  • 계좌 목록 조회 & 생성 -> http://127.0.0.1:8000/accounts/
  • 특정 계좌 조회/수정/삭제 -> http://127.0.0.1:8000/accounts/1/

거래 (transactions)

  • 거래 목록 조회 & 생성 -> http://127.0.0.1:8000/transactions/
  • 특정 거래 조회/수정/삭제 -> http://127.0.0.1:8000/transactions/1/

필터

카테고리

  • http://127.0.0.1:8000/transactions/?category=food → 식비만 보기
  • http://127.0.0.1:8000/transactions/?category=shopping → 쇼핑만 보기

기간

start = self.request.query_params.get("start")
end = self.request.query_params.get("end")

if start:
    qs = qs.filter(transacted_at__gte=start)
if end:
    qs = qs.filter(transacted_at__lte=end)
  • 특정 날짜 이후 거래만 보기

    • http://127.0.0.1:8000/transactions/?start=2025-01-01
  • 특정 날짜 이전 거래만 보기

    • http://127.0.0.1:8000/transactions/?end=2025-01-31
  • 특정 기간 전체 보기

    • http://127.0.0.1:8000/transactions/?start=2025-01-01&end=2025-01-31

카테고리 + 기간

  • 식비 중에서 2025년 1월 1일 ~ 2025년 1월 15일 거래만 보기

  • http://127.0.0.1:8000/transactions/?category=food&start=2025-01-01&end=2025-01-15

정렬

ordering_fields = ['transacted_at', 'amount']
  • 식비 중에 최근 거래순 정렬

    • /transactions/?category=food&ordering=-transacted_at
  • 쇼핑 중 금액 적은순 정렬

    • /transactions/?category=shopping&ordering=amount
  • 기간+카테고리+정렬 모두 적용

  • /transactions/?category=transport&start=2025-01-01&end=2025-01-31&ordering=-amount

drf-yasg 설정

    1. poetry add drf-yasg
    1. settings.py 설정 → INSTALLED_APPS'drf_yasg'
    1. core/urls.py 수정
    1. http://127.0.0.1:8000/swagger/ / http://127.0.0.1:8000/redoc/

Filterset

  • FilterSet 적용시 이점

    • 1) DRF 브라우저 페이지에 필터 UI 자동 생성됨 (카테고리 드롭다운, 날짜 입력칸이 나타남)
    • 2) 프론트 개발 시 API 호출만 하면 됨 → React / Vue / Next.js에서 그대로 사용됨
    • 3) 코드 유지보수비용 감소 → 조회 기능이 깔끔하게 정리됨

day 2 🧐

코드 리뷰 / 부가 기능


코드 리뷰 📝

def recalc_account_transactions(account):
    transactions = account.transactions.order_by('transaction_at', 'id')

    balance = 0
    for tx in transactions:
        if tx.type == "INCOME":
            balance += tx.amount
        else:
            balance -= tx.amount

        tx.balance_after = balance
        tx.save()

  • 1) 거래 삭제 시 auto rollback

def perform_destroy(self, instance):
    account = instance.account
    instance.delete()

    # 삭제 후 전체 balance_after 다시 계산
    recalc_account_transactions(account)

  • 2) 거래 수정 시 balance_after 재계산

def perform_update(self, serializer):
    instance = self.get_object()
    account = instance.account

    # 새 값으로 저장
    serializer.save()

    # 수정 후 전체 거래 balance_after 재계산
    recalc_account_transactions(account)

day1 코드 리뷰

  • accounts/urls.py

    • path('', AccountListCreateView.as_view(), name='account_list')
      • /accounts/ 로 들어오면 계좌 목록 조회 + 계좌 생성
    • path('<int:pk>/', AccountDetailView.as_view(), name='account_detail')
      • /accounts/3/ 같은 경로에서 특정 계좌 조회 / 수정 / 삭제
  • accounts/views.py

    • AccountListCreateView

      • ListCreateAPIView → GET(목록), POST(생성)
      • IsAuthenticated → 인증 필요
      • get_queryset() → 로그인한 사용자 본인의 계좌만 조회
      • perform_create() → 계좌 생성 시 자동으로 user 설정
    • AccountDetailView

      • RetrieveUpdateDestroyAPIView → GET/PUT/PATCH/DELETE 가능
      • 유저의 계좌만 조회 가능
  • transactions/urls.py

    • /transactions/ → 목록 조회 + 생성

    • /transactions/3/ → 조회 + 수정 + 삭제


day2 코드 리뷰

  • 거래(Transaction) 생성 시 잔액(balance_after)을 자동 계산

  • 계좌 소유자 검증까지 처리하는 핵심 로직


    def perform_create(self, serializer):
        account = serializer.validated_data['account']
  • 요청(body)에 들어온 account 값(FK)을 꺼내는 부분

    • 검증(validated)된 account 객체를 가져옴

        if account.user != self.request.user:
            raise PermissionDenied("이 계좌에 거래를 생성할 수 없습니다.")
  • 계좌 소유자 검증

    • 거래하려는 계좌(account.user) | 로그인한 사용자(self.request.user)
    • 두 사용자가 다른 경우 → 해당 계좌에 거래 생성 금지

        # 계좌의 최신 balance_after = 최근 거래 balance_after
        last_transaction = (
            account.transactions.order_by('-transaction_at', '-id').first()
        )
  • 마지막(가장 최근) 거래 찾기

    • 의미: 해당 계좌(account)의 모든 거래 중 날짜 내림차순 / id 내림차순 정렬()
      • 가장 맨 위 한 개 = 최신 거래 / -id = 거래시간이 같을 경우 id값이 큰것이 최신

        last_balance = last_transaction.balance_after if last_transaction else 0
  • 마지막 잔액 값 가져오기

    • 이전에 거래가 있으면 → 마지막 거래의 balance_after 사용
    • 거래가 아예 없으면 → 초기 잔액을 0으로 설정

        amount = serializer.validated_data['amount']
        type = serializer.validated_data['type']
  • amount / type 가져오기

    • 요청에서 받은 금액(amount)과 거래 타입(type: INCOME | EXPENSE) 가져옴

        # 새 잔액 계산
        if type == 'INCOME':
            new_balance = last_balance + amount
        else:
            new_balance = last_balance - amount

        # 저장
        serializer.save(
            user=self.request.user,
            balance_after=new_balance
        )
  • 새로운 잔액 계산

    • 입금(INCOME) → 더하기 / 출금(EXPENSE) → 빼기 -> 즉, balance_after(거래 후 잔액)를 계산
  • serializer.save()에서 balance_after 저장

    • 핵심 3가지 데이터를 저장
      • user: 로그인한 사용자 / balance_after: 계산된 잔액
      • 나머지 validated_data 필드들 (amount, type, account, category...)

부가 기능

현재 잔액 조회

  • GET /accounts/{id}/balance/

# accounts/views.py
class AccountBalanceView(GenericAPIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, pk):
        # 1) 본인 계좌인지 확인
        try:
            account = Account.objects.get(pk=pk, user=request.user)
        except Account.DoesNotExist:
            return Response(
                {"detail": "계좌를 찾을 수 없습니다."},
                status=status.HTTP_404_NOT_FOUND
            )

        # 2) 해당 계좌의 최신 거래 조회
        last_tx = account.transactions.order_by('-transaction_at', '-id').first()

        # 3) 거래 없으면 잔액=0
        balance = last_tx.balance_after if last_tx else 0

        # 4) 반환
        return Response({
            "account_id": account.id,
            "account_name": account.name,
            "current_balance": balance
        })

# accounts/urls.py
path('<int:pk>/balance/', AccountBalanceView.as_view(), name='account_balance'),

day 3 🥲

Pandas / Matplotlib / Celery / 코드리뷰

참고 문서


4단계

  • Pandas, Matplotlib, Celery 를 이용해서 매주, 매월 소비습관 분석하기

    • 데이터 시각화에 주로 사용되는 라이브러리인 Matplotlib, Pandas
      • 를 이용해서 매주, 매월 단위의 총 소비금액을 비교, 분석하는 그래프를 그려보기
        • Pandas = 데이터를 정리, 분석 / Matplotlib = 데이터를 그래프로 시각화
    • 또한, 비동기 작업 및 작업 대기열을 관리해주는 라이브러리인 Celery
      • 를 백그라운드에서 활용하여 스케줄링을 통해 매주 특정 요일&매월 특정 날짜마다
      • 그래프를 그리는 작업을 수행하도록 스케줄링 작업을 지정

Pandas (판다스)

  • 데이터를 다루기 위한 파워풀한 도구 — “엑셀을 파이썬으로 더 강력하게 쓰는 느낌”

  • 기능

    • 표 형태 데이터(엑셀, CSV 등) 다루기
    • 행/열 필터링, 정렬, 그룹화 등 데이터 전처리
    • 누락된 값 처리
    • 데이터 집계(총합, 평균, 최대/최소 등) ⭐️
    • 시간/날짜별 데이터 다루기
      • Django에서 DB에서 가져온 데이터를 통계로 만들 때 Pandas가 강력해짐
  • 사용법

# 1. 데이터 불러오기 
import pandas as pd

df = pd.read_csv('transactions.csv')

# 2. 특정 열만 보기
df['amount']

# 3. 조건 필터링
df[df['type'] == 'EXPENSE']

# 4. 그룹별 합계 계산(ex. 카테고리별 지출 )
df.groupby('category')['amount'].sum()

Matplotlib (맷플롯립)

  • 데이터를 그래프로 그려주는 라이브러리 — Pandas와 세트로 자주 쓰임

  • 기능

    • 선 그래프(Line plot) / 막대 그래프(Bar chart)
    • 파이 차트(Pie chart) / 히스토그램
    • 트렌드 라인 / 데이터 시각화
      • 잔액 그래프, 월별 지출 그래프 만들 때 바로 사용
  • 사용법

import matplotlib.pyplot as plt

plt.plot([1, 3, 2, 6, 4])  # 그래프 그리기
plt.title("잔액 변화 그래프")
plt.xlabel("날짜")
plt.ylabel("잔액")
plt.show()

# 막대 그래프 예시
categories = ['식비', '교통', '쇼핑']
amounts = [12000, 8000, 15000]

plt.bar(categories, amounts)
plt.title("카테고리별 지출")
plt.show()


Pandas + Matplotlib

# 월별 지출 그래프 
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('transactions.csv')

monthly = df.groupby('month')['amount'].sum()

plt.plot(monthly.index, monthly.values)
plt.title("월별 지출 변화")
plt.show()
  • 정석 루트: Pandas로 집계 → Matplotlib으로 시각화

Celery

[클라이언트] → Django API → (Celery에게 작업 전달) → 바로 응답  
                             ↓
                       [Celery Worker]  
                  (= 백그라운드 작업 처리)
  • 파이썬 비동기 작업 처리(Background Task) 전문 툴

    • Django + DRF 개발할 때 무거운 작업을
      • 백그라운드에서 처리해야 할 때 거의 필수로 사용하는 라이브러리
      • 요약하자면, Celery = “백그라운드에서 돌아가는 작업자(Worker)”
  • 기능

      1. 오래 걸리는 작업을 백그라운드에서 실행
      • 회원가입 시 이메일 인증메일 보내기
      • 주문이 들어오면 문자 알림 보내기
      • 업로드된 이미지 자동 리사이즈
      1. 주기적인 작업 예약 (Cron 같은 기능)
      • 매일 00시에 DB 정리
      • 매 10분마다 뉴스 크롤링
      • 매일 9시에 출석체크 알림 전송
      • 매주 특정 요일&매월 특정 날짜마다 그래프를 그리는 작업 시작
        • Celery + Celery Beat 조합으로 가능
  • 간단한 예시

from celery import shared_task
import time

@shared_task
def slow_task():
    time.sleep(10)
    print("10초 작업 완료")

# Django에서 호출
## → Django는 바로 응답하고, Celery가 백그라운드에서 실행
slow_task.delay() 
  • Celery 사용시 필수

    • Celery를 사용할 때 Redis(또는 RabbitMQ) 는 거의 필수(보통 Redis를 가장 많이 사용)
  • 이유

    • Celery는 작업을 전달받고 처리하는 방식이 “메시지 큐(Message Queue)” 구조
    • 이 메시지 큐 역할을 Redis 가 가장 간단하게 해주기 때문
  • Celery 구조

    Django (작업 요청)
           ↓
       [메시지 큐]  ← Redis or RabbitMQ
           ↓
    Celery Worker (작업자)
    
    • Django ↔ Celery worker 사이의 “작업 전달”을 담당하는 게 Redis임.
    • Celery는 자체 메시지 큐가 없고 반드시 외부 메시지 브로커(redis, rabbitmq 등) 가 필요
      • Celery는 브로커 없이 작동하지 않음
  • Redis 역할

    • 1) Celery 메시지 큐 역할
      • “할 일 목록 저장소”
      • Django에서 task.delay() 하면 Redis에 작업이 저장됨
      • Worker가 Redis에서 작업을 꺼내와 처리
    • 2) Celery 결과 저장(옵션)
      • 예: "백그라운드 작업 결과 조회"

시작 🎬

Analysis 앱 만들기

  • Analysis App 생성과 Analysis 모델 구현하기


코드 리뷰 📝

serializers.py

1. Analysis 모델을 JSON 변환/검증하기 위한 Serializer 선언.
class AnalysisSerializer(serializers.ModelSerializer):

2. Analysis 모델의 모든 필드를 포함하도록 설정  
    class Meta:
        model = Analysis
        fields = "__all__"

views_html.py

  • 분석 목록 페이지

@login_required
- 1. 로그인한 사용자만 접근하도록 제한하는 데코레이터

def analysis_list_html(request):
- 2. 분석목록 페이지	

    user = request.user
    - 3. 현재 요청 보낸 사용자
    
    period = request.GET.get("period")  
    - 4. URL 파라미터로 period 값 가져옴.

    qs = Analysis.objects.filter(user=user).order_by("-created_at")
	- 5. 본인 데이터만 조회 → 최신순 정렬.
    
# period 조건 적용

    if period == "DAILY":
        start, end = get_daily_range() ↔ utils에 만들어 놓은 함수임 
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "LAST_DAY":
        start, end = get_last_day_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "WEEKLY":
        start, end = get_week_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "LAST_WEEK":
        start, end = get_last_week_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "MONTHLY":
        start, end = get_month_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "LAST_MONTH":
        start, end = get_last_month_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "YEARLY":
        start, end = get_year_range()
        qs = qs.filter(period_start=start, period_end=end)

    elif period == "LAST_YEAR":
        start, end = get_last_year_range()
        qs = qs.filter(period_start=start, period_end=end)

    return render(request, "analysis/analysis_list.html", {
        "analyses": qs
    })
  • 분석 생성 페이지

@login_required
def analysis_create_html(request):
- 1. 분석 생성 페이지

    result = None
    - 2. 기본값 설정

    if request.method == "POST":
        analysis_type = request.POST.get("type")
        - 3. 어떤 기간을 눌렀는지 확인

        # 기간 계산
        mapping = {
            "DAILY": get_daily_range,
            "LAST_DAY": get_last_day_range,
            "WEEKLY": get_week_range,
            "LAST_WEEK": get_last_week_range,
            "MONTHLY": get_month_range,
            "LAST_MONTH": get_last_month_range,
            "YEARLY": get_year_range,
            "LAST_YEAR": get_last_year_range,
        }

        if analysis_type not in mapping:
        - 4. 잘못된 타입이면 에러
        
            return render(request, "analysis/analysis_create.html", {"error": "유효하지 않은 타입"})

        start, end = mapping[analysis_type]()
        - 5. 날짜 범위 계산 = 이름에 맞는 함수 실행

        analyzer = Analyzer(
            user=request.user,
            about="TOTAL_SPENDING",
            period_type=analysis_type,
            start_date=start,
            end_date=end,
        )

        result = analyzer.run()
        - 6. 분석 실행 후 DB 저장 + 이미지 저장

views.py (API)

  • 조회 (ListAPIView)

class AnalysisListView(ListAPIView):
    serializer_class = AnalysisSerializer
    - 1. 응답을 직렬화할 때 사용
    permission_classes = [IsAuthenticated]
	- 2. 로그인 필요
    
    def get_queryset(self):
    - 3. 어떤 데이터를 보여줄지 결정
    
        user = self.request.user
        qs = Analysis.objects.filter(user=user).order_by("-created_at")

        period = self.request.query_params.get("period")

        if period == "DAILY":
            start, end = get_daily_range()
            qs = qs.filter(period_start=start, period_end=end)

        elif period == "LAST_DAY":
            start, end = get_last_day_range()
            qs = qs.filter(period_start=start, period_end=end)

        .
        .
        .

        return qs
  • 생성 (APIView)

- 1. 단일 POST 기능이므로 APIView 사용
class AnalysisCreateView(APIView):
    permission_classes = [IsAuthenticated]
    
    - 2. 생성 요청 처리
    def post(self, request):  
    
    - 3. 클라이언트가 보낸 정보 읽기.
		user = request.user
        analysis_type = request.data.get("type")
        about = request.data.get("about", "TOTAL_SPENDING")
        description = request.data.get("description", "")


        # 기간 계산
        if analysis_type == "DAILY":
            start_date, end_date = get_daily_range()
        elif analysis_type == "LAST_DAY":
            start_date, end_date = get_last_day_range()
        elif analysis_type == "WEEKLY":
            start_date, end_date = get_week_range()
        elif analysis_type == "LAST_WEEK":
            start_date, end_date = get_last_week_range()
        elif analysis_type == "MONTHLY":
            start_date, end_date = get_month_range()
        elif analysis_type == "LAST_MONTH":
            start_date, end_date = get_last_month_range()
        elif analysis_type == "YEARLY":
            start_date, end_date = get_year_range()
        elif analysis_type == "LAST_YEAR":
            start_date, end_date = get_last_year_range()
        else:
            return Response({"error": "Invalid type"}, status=400)

		- 4. 분석기 생성
        analyzer = Analyzer(
            user=user,
            about=about,
            period_type=analysis_type,
            start_date=start_date,
            end_date=end_date,
            description=description,
        )

        result = analyzer.run()
		
        - 5. 생성된 분석 결과를 JSON으로 반환
        return Response({
            "id": result.id,
            "type": result.type,
            "about": result.about,
            "period_start": result.period_start,
            "period_end": result.period_end,
            "image_url": result.result_image.url if result.result_image else None,
        })

urls.py

urlpatterns = [
    # API
    path("api/", AnalysisListView.as_view(), name="analysis_list"),
    - 1. GET /analysis/api/
    
    path("api/create/", AnalysisCreateView.as_view(), name="analysis_create"),
	- 2. POST /analysis/api/create/
    
    # 템플릿용 HTML 페이지.
    path("html/", analysis_list_html, name="analysis_html"),
    path("html/create/", analysis_create_html, name="analysis_create_html"),
]

analyzers.py

Agg

import matplotlib
matplotlib.use("Agg")
  • 서버 환경에서 그래프를 화면에 띄우지 않고 이미지 파일로 생성만 하기 위해 사용하는 필수 설정
  • 웹 서버(Uvicorn, Gunicorn 등)에서는 GUI가 없어서 Agg 백엔드를 지정해야 오류가 안 남

  • import / from

import os
import uuid
import pandas as pd
import matplotlib.pyplot as plt
from django.conf import settings

class Analyzer:

	- 1. init : 초기 설정 = 분석 요청 시 전달되는 메타정보 저장 ⭐️
    def __init__(self, user, about, period_type, start_date, end_date, description=""):
        self.user = user
        self.about = about
        self.period_type = period_type
        self.start_date = start_date
        self.end_date = end_date
        self.description = description

        - 2. MEDIA_ROOT/analysis/유저ID/ 경로에 저장.
        self.output_dir = os.path.join(settings.MEDIA_ROOT, "analysis", str(user.id))
        - 2-1. 각 사용자별 폴더를 자동 생성
        os.makedirs(self.output_dir, exist_ok=True)
	
    - 3. DB에서 해당 기간 데이터 조회 ⭐️
    def get_transactions(self):
        from transactions.models import Transaction

        qs = Transaction.objects.filter(
            user=self.user,
            transacted_at__date__gte=self.start_date,
            transacted_at__date__lte=self.end_date,
        ).values("transacted_at", "amount", "type", "category")
        - 4. .values() 를 사용해 QuerySet → 단순 dict 리스트로 가져옴
        - 4-1. pandas가 바로 DataFrame으로 변환 가능하게 하기 위함

        return pd.DataFrame(qs)
		- 5. DataFrame 형태로 바로 분석할 수 있게 반환
        
    - 6. 날짜 변환 처리 ⭐️    
    def build_dataframe(self):
        df = self.get_transactions()

        if df.empty:
            return df
		
        - 7. 날짜 비교 & 그래프 등을 위해 datetime 타입으로 변환.
        df["transacted_at"] = pd.to_datetime(df["transacted_at"])
        return df

    - 7. 수입/지출/차액 계산 ⭐️
    def calculate_totals(self, df):
    	- 8. type이 “deposit”(입금)인 row들의 amount 합
        total_income = df[df["type"] == "deposit"]["amount"].sum()
        - 9. type이 “withdraw”(출금)인 row들의 amount 합
        total_spending = df[df["type"] == "withdraw"]["amount"].sum()
        - 10. 차액 계산 = 수입 - 지출
        difference = total_income - total_spending
        return total_income, total_spending, difference

    - 11. 새 그래프: 수입 vs 지출 비교 ⭐️⭐️
    def build_plot(self, total_income, total_spending):
        - 12. 그래프 크기 설정
        plt.figure(figsize=(6, 4)) 
		
        labels = ["Income", "Spending"]
        values = [total_income, total_spending]

        - 13. 단순 바(bar) 그래프 형태 / 수입은 초록, 지출은 빨강
        plt.bar(labels, values, color=["green", "red"])
        
        - 14. 제목, y축 표시, 여백 자동 조정.
        plt.title("Income vs Spending")
        plt.ylabel("Amount")
        plt.tight_layout()
	
    15. 그래프 이미지로 저장 ⭐️
    def save_plot_as_image(self):
    
    	- 16. uuid4()를 사용하여 충돌 없는 파일 이름 생성
        filename = f"{uuid.uuid4()}.png"
        filepath = os.path.join(self.output_dir, filename)
		
        - 17. 이미지 파일로 저장 / 메모리에서 그래프 객체 제거
        plt.savefig(filepath)
        plt.close()

        return f"analysis/{self.user.id}/{filename}"

    - 18. Analysis 모델 생성(DB 저장)
    def save_analysis_model(self, image_path, total_income, total_spending, difference):
        from .models import Analysis
        analysis = Analysis.objects.create(
            user=self.user,
            about=self.about,
            type=self.period_type,
            period_start=self.start_date,
            period_end=self.end_date,
            description=self.description,
            result_image=image_path,

            total_income=total_income,
            total_spending=total_spending,
            difference=difference,
        )
        return analysis

	- 19. 전체 프로세스를 순서대로 진행 ⭐️⭐️⭐️
    def run(self):
        df = self.build_dataframe()
        if df.empty:
            raise ValueError("해당 기간에 분석 가능한 거래가 없습니다.")

        # 수입/지출/차액 계산
        total_income, total_spending, difference = self.calculate_totals(df)

        # 그래프 생성
        self.build_plot(total_income, total_spending)

        # 그래프 저장
        image_path = self.save_plot_as_image()

        # 모델 저장 (수치 포함)
        return self.save_analysis_model(image_path, total_income, total_spending, difference)

전체 흐름 정리

run()
 └─ build_dataframe()
      └─ get_transactions()  → DataFrame
 └─ calculate_totals()       → income/spending/difference
 └─ build_plot()             → 그래프 생성
 └─ save_plot_as_image()     → 이미지 저장, 경로 리턴
 └─ save_analysis_model()    → DB 저장
 └─ 최종 Analysis 객체 반환

day 4 😩

필터 html / Celery / Django Signal


메인페이지 디자인

  • 메인페이지 위에 분석 요약 작게 만들어 놓기

분석파트 기능 추가

  • 분석할게 없을경우 나올 화면 만들기


메인페이지 필터


  • 이미 코드구현은 되어있는 상태
    • DRFView 에는 구현되어 있으나 HTMLView 에는 구현이 안되있는 상태
    • 즉, API 화면에서는 필터 적용 가능하지만, HTML 템플릿에서는 별도 처리 없어서 작동 X
# transaction/view_html.py
@login_required
def transaction_html_view(request):
    qs = Transaction.objects.filter(user=request.user)

    # 카테고리 필터
    category = request.GET.get("category")
    if category:
        qs = qs.filter(category=category)

    # 기간 필터
    start = request.GET.get("start")
    end = request.GET.get("end")

    if start:
        qs = qs.filter(transacted_at__date__gte=start)
    if end:
        qs = qs.filter(transacted_at__date__lte=end)

    qs = qs.order_by('-created_at')

    return render(request, "transactions/transaction_list.html", {
        "transactions": qs,
    })
# templates/transaction/transaction_list.html
<!-- 필터기능 추가 -->
<div class="card shadow-sm p-3 mb-4">
  <form method="GET" class="row g-3">

    <!-- 카테고리 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">카테고리</label>
      <select name="category" class="form-select">
        <option value="">전체</option>
        <option value="food" {% if request.GET.category == "food" %}selected{% endif %}>식비</option>
        <option value="transport" {% if request.GET.category == "transport" %}selected{% endif %}>교통</option>
        <option value="shopping" {% if request.GET.category == "shopping" %}selected{% endif %}>쇼핑</option>
        <option value="income" {% if request.GET.category == "income" %}selected{% endif %}>수입</option>
        <option value="etc" {% if request.GET.category == "etc" %}selected{% endif %}>기타</option>
      </select>
    </div>

    <!-- 시작 날짜 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">시작 날짜</label>
      <input type="date"
             name="start"
             class="form-control"
             value="{{ request.GET.start }}">
    </div>

    <!-- 종료 날짜 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">종료 날짜</label>
      <input type="date"
             name="end"
             class="form-control"
             value="{{ request.GET.end }}">
    </div>

    <!-- 버튼 -->
    <div class="col-md-3 d-flex align-items-end">
      <button class="btn btn-dark w-100 py-2">
        🔍 필터 적용
      </button>
    </div>

  </form>
</div>

Celery

  • 비동기 작업 및 작업 대기열을 관리하는 Python 기반의 분산 메시지 큐 시스템

  • 주로 백그라운드 작업 처리, 비동기 작업 처리, 그리고 스케줄링된 작업을 처리하는 데 사용

  • Celery는 분산 시스템이기 때문에 여러 노드에서 동시에 작업을 처리할 수 있어 확장성이 뛰어나며,

    • 주로 웹 애플리케이션에서 장시간 소요되는 작업을 백그라운드에서 처리하는 데 자주 사용됨
  • 작동 요약

      1. 클라이언트가 작업을 생성하고 브로커에 전송
      1. 브로커는 받은 작업을 큐에 저장
      1. 워커는 브로커의 큐를 모니터링하고 있다가 새 작업이 들어오면 가져감
      1. 워커가 작업을 실행
      1. 작업 결과는 결과 백엔드에 저장(설정시)
      1. 클라이언트는 필요애ㅔ 따라 결과 백엔드에서 작업 상태나 결과를 조회

  • 주요기능

      1. 비동기 작업 처리
      • Celery는 작업을 비동기로 처리 가능
      • 즉, 클라이언트가 요청을 보내면 즉시 응답을 반환하고, 작업은 백그라운드에서 처리됨
        • 이로 인해 사용자는 작업이 끝날 때까지 기다릴 필요가 없음
      1. 예약된 작업 처리 (Periodic Task)
      • Celery는 작업을 일정 시간 간격으로 실행 가능
      • 이 기능을 위해 celery beat라는 스케줄러가 필요
        • 예를 들어, 매일 자정에 데이터를 백업하는 작업을 예약 가능
      1. 확장성 (Scalability)
      • Celery는 매우 높은 확장성을 제공
      • 여러 대의 워커가 동시에 작업을 처리할 수 있으며, 필요에 따라 워커를 추가해 작업 처리 능력을 확장
      1. 내결함성 (Fault Tolerance)
      • Celery는 작업 실패에 대비해 재시도 기능을 제공함
      • 만약 작업이 실패하면 일정 시간 후 다시 시도하게 할 수 있음
      1. 타임아웃 설정
      • Celery는 작업에 시간 제한을 설정할 수 있어,
        • 특정 시간이 초과되면 작업을 중단하거나 실패 처리할 수 있음

  • 아키텍쳐

    • 프로듀서 (Producer)
      • 프로듀서는 작업을 생성하고, 이를 대기열에 넣는 역할
      • Django, Flask 같은 웹 애플리케이션이 주로 프로듀서 역할을 하며,
        • 사용자가 요청한 작업을 Celery로 보내 대기열에 등록
    • 브로커 (Broker)
      • Celery가 사용하는 메시지 브로커는 작업을 대기열에 저장, 워커가 이 작업을 가져갈 수 있게 관리
      • RedisRabbitMQ가 이 역할을 자주 수행
    • 컨슈머 (Consumer, 워커)
      • 워커는 대기열에 있는 작업을 가져가서 실행하는 역할
      • 워커는 여러 개의 프로세스나 머신에서 실행될 수 있으며, 이를 통해 동시에 많은 작업을 처리 가능
    • 결과 백엔드 (Result Backend)
      • 작업이 완료되면 결과를 저장하는 시스템으로, 클라이언트가 요청한 작업의 결과를 추적할 수 있게 해줌

Celery 예시

    1. 예약된 작업 처리
    from celery.schedules import crontab
    
    app.conf.beat_schedule = {
        'add-every-midnight': {
            'task': 'tasks.add',
            'schedule': crontab(minute=0, hour=0),
            'args': (16, 16),
        },
    }
    1. 기본 설정
    from celery import Celery
    
    app = Celery('my_project', broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/0')
    
    @app.task
    def add(x, y):
        return x + y
    1. 작업 호출
      result = add.delay(4, 6)

  • Celery 를 사용해야 할 때

    • 웹 애플리케이션에서 시간이 오래 걸리는 작업을 백그라운드에서 처리하고 싶을 때
      • (예: 이미지 처리, 이메일 전송)
    • 주기적으로 실행되어야 하는 작업을 자동화할 때
      • (예: 정기 백업, 데이터 수집)
    • 여러 서버에서 동시에 작업을 분산 처리하고 싶을 때

  • Celery 와 함께 자주 사용하는 도구

    • Redis 또는 RabbitMQ:
      • 메시지 브로커로 자주 사용됨
    • Flower
      • Celery의 실시간 모니터링 도구로, 작업 상태를 시각적으로 관리
    • Django
      • Celery는 Django 프로젝트와 매우 잘 통합되어 백그라운드 작업 처리에 자주 사용됨

장점

  • 분산 시스템으로 확장 가능
  • 다양한 백엔드 및 메시지 브로커 지원
  • 비동기 및 동기 작업 모두 지원
  • 주기적인 작업 스케줄링 지원

단점

  • 복잡한 설정 및 운영이 필요할 수 있음
  • 작업 실패 시 디버깅이 까다로울 수 있음

django-celery-beat

  • Celery를 이용해 등록한 Task를 스케줄링 작업으로 변환하는 라이브러리
  • Crontab, Periodick Task, Solar Event, Interval 등의 작업 속성 주기를 택하여
    • Task를 선택한 주기에 맞게 수행하도록 함
  • 등록된 스케줄링 작업은 장고 어드민에서 확인 가능하며,
    • 어드민 페이지에서 스케줄링 작업을 생성, 변경, 삭제도 가능

django-celery-results

  • Celery에 등록된 작업 Task의 수행결과를 데이터베이스에 저장
    • 해당 결과는 Django Admin에서 확인 가능

Celery 세팅 ⚙️

  • Celery를 이용하여 스케줄링 작업 구현하기

  • poetry add django-celery-beat

    • Python 3.13에서는 django-celery-beat 를 설치 불가
      • django-celery-beat가 내부적으로 사용하는 django-timezone-field 라이브러리가
      • Python < 4.0 만 지원 -> requires-python = ">=3.13,<4.0" 버전을 명시

Redis

  • brew

    • 설치: brew install redis
    • 실행: brew services start redis
  • docker

    • docker run -d --name redis -p 6379:6379 redis:7

기본 Celery 설정

  • celery.py 파일을 core/ 에 넣고 __init__.pyCelery App 연결한 이유
    • manage.py 와 같은 디렉토리(= settings.py 가 들어있는 디렉토리)에 넣어야 하기 때문
  • Celery가 Django 설정을 로딩하려면

    • os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moneybook.settings")
      • 이렇게 설정을 읽어야 함 -> 그러기 위해 celery.py도 Django 프로젝트 패키지 안에 있어야 함
      • 이렇게 함으로써, Celery가 settings.py를 올바르게 import 가능
      • 즉, Celery가 Django settings.py를 로딩해야 하기 때문에 core/ 에 추가함

app_task / shared_task

구분@app.task@shared_task
Celery 앱 필요?필요함 (app 인스턴스 기반)필요 없음 (프로젝트 어디서나 사용 가능)
추천 용도단일 앱 형태의 Celery 구성Django의 여러 앱에서 task 를 사용할 때
장점명시적이고 구조적임Django 프로젝트에서 가장 많이 사용됨
  • 프로젝트 전체에서 Celery 앱을 공유하기 때문에 Django에서는 대부분 @shared_task 가 정석

Celery 실습 📘

Celery 분석 자동 생성 기능 만들기

  • 1. analysis/tasks.py

from celery import shared_task
from django.contrib.auth import get_user_model
from .analyzers import Analyzer
from .utils import get_daily_range

User = get_user_model()

@shared_task
def create_daily_analysis(user_id):
    """특정 사용자에 대한 DAILY 분석을 자동으로 생성"""
    user = User.objects.get(id=user_id)
    start_date, end_date = get_daily_range()

    analyzer = Analyzer(
        user=user,
        about="TOTAL_SPENDING",
        period_type="DAILY",
        start_date=start_date,
        end_date=end_date,
        description="자동으로 생성된 매일 분석",
        graph_type="CATEGORY",
    )

    result = analyzer.run()
    return f"Analysis created: {result.id}"

  • 2. Celery worker와 beat 실행하기

    • 터미널을 2개 열고 아래 두 개 실행해야 한다
    • 해석

      • Django 서버(runserver)는 웹 요청만 처리한다.
      • Celery는 백그라운드 작업(비동기/스케줄링)을 처리한다.
      • 즉, Celery는 2개의 엔진을 따로 실행해야 한다.
    • 두개의 엔진

      • 1) Worker -> 작업을 실제로 처리하는 사람(일꾼)
        • ex. 분석 생성 / 그래프 이미지 저장 / 이메일 발송 / 무거운 계산
      • 2) Beat -> 스케줄러(일정 관리자)
        • ex. 매일 00:00 자동 분석 생성 / 매시간 데이터 수집 / 매주 보고서 생성
      • 즉, 터미널을 두개 켜야 함

  • 3. 명령어

    • celery -A core worker --loglevel=INFO

    • celery -A core beat --loglevel=INFO


  • 4. Django에서 Celery Task 직접 테스트하기

    • Celery가 잘 동작하는지 먼저 “수동 실행”으로 테스트함

  • 5. API/HTML 버튼을 Celery 기반으로 바꿔주기 (선택)

    • 현재 "분석 생성" 버튼은 Django에서 바로 analyzer 실행 → 시간이 오래 걸림.
    • Celery로 바꾸면 즉시 응답하고, 백그라운드에서 분석 생성함.
      • 이 기능을 적용하면, 버튼을 누르면 “분석 생성 중…”으로 유지되고,
        • 백그라운드에서 Celery가 그래프 + 분석 저장을 해줌.
from analysis.tasks import create_daily_analysis

class AnalysisCreateView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        task = create_daily_analysis.delay(request.user.id)
        return Response({
            "message": "분석 생성이 요청되었습니다.",
            "task_id": task.id
        })

  • 6. 자동 스케줄링 등록하기 (진짜 Celery의 핵심 기능)

  • 17:30 분에 테스트 완료
    • 이 기능이 유지되기 위해서는 worker / beat 둘다 켜져있어야 함 (현제는 딱히 유용하지는 않을 듯)

Django Signal

Django 공식문서 - signal


Django Signal

  • 특정 이벤트가 발생할 때 다른 부분에서 알림을 받아 추가 작업을 수행할 수 있도록 하는 기능

    • 이 시그널은 이벤트 기반 프로그래밍의 한 형태로,
      • 모델의 저장, 삭제 등의 중요한 작업이 발생할 때 추가적인 로직을 실행 가능
  • 사용 ex.

    • 모델이 저장되거나 삭제될 때 이를 감지하여 다른 작업을 트리거하는 것
    • Django는 이런 이벤트들을 처리하기 위해 여러 내장 시그널을 제공하며,
      • 사용자 정의 시그널을 만들 수도 있음

주요 개념

  • 시그널 (Signal)
    • 시그널은 특정 이벤트가 발생했을 때 발송됨
      • 예를 들어, 모델의 인스턴스가 저장될 때 시그널이 발송될 수 있음
    • Django는 다양한 내장 시그널을 제공하며, 사용자는 이를 직접 정의할 수도 있음
  • 수신기 (Receiver)
    • 수신기는 시그널을 받아 처리하는 함수
      • 시그널이 발송될 때 이를 수신하여 특정 동작을 수행하는 역할을 함
    • 일반적으로 시그널을 정의하고 나면, 이 시그널에 수신기를 등록하여 시그널을 처리
  • 발송자 (Sender)
    • 시그널이 발송될 때, 어떤 객체나 클래스가 시그널을 발송했는지를 나타냄
    • 시그널을 특정 발송자와 연결하여, 그 발송자에서 발생하는 이벤트에 대해서만 시그널을 수신 가능

Django 내장 시그널

  • Django는 몇 가지 내장 시그널을 제공 / 가장 자주 사용되는 시그널 다음 7가지.
      1. pre_save: 모델의 인스턴스가 저장되기 에 호출
      1. post_save: 모델의 인스턴스가 저장된 에 호출
      1. pre_delete: 모델의 인스턴스가 삭제되기 에 호출
      1. post_delete: 모델의 인스턴스가 삭제된 에 호출
      1. m2m_changed: 다대다 관계가 변경될 때 호출
      1. request_started: HTTP 요청이 시작될 때 호출
      1. request_finished: HTTP 요청이 끝났을 때 호출

시그널 사용 방법

1. 시그널 정의 및 수신기 연결

  • 시그널을 사용하기 위해서는 수신기(Receiver)를 정의하고, 이 수신기를 특정 시그널에 연결 필요
  • Django에서 시그널을 정의할 필요는 없으며,
    • 내장 시그널을 사용하거나 필요에 따라 사용자 정의 시그널을 만들 수 있음

2. 수신기 함수 작성

  • 수신기 함수는 시그널이 발송될 때 호출되는 함수
    • 수신기 함수는 반드시 두 개의 인자를 받아야 함 : sender / **kwargs.
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 수신기 함수 정의
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    if created:
        print(f"{instance}이(가) 생성되었습니다!")
    else:
        print(f"{instance}이(가) 수정되었습니다!")

3. 시그널과 수신기 연결

  • 시그널과 수신기를 연결하기 위해 @receiver 데코레이터를 사용 가능
    • 이 데코레이터는 특정 시그널이 발송될 때 해당 수신기 함수를 실행하도록 설정
    • 또는 signals.connect() 메서드를 사용해 시그널과 수신기를 명시적으로 연결도 가능
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 시그널과 수신기 연결
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    # 저장된 후 처리할 로직
    if created:
        print(f"새 인스턴스 {instance}가 생성되었습니다!")

사용자 정의 시그널

  • 내장 시그널 외에도 사용자 정의 시그널을 만들어 특정 이벤트가 발생했을 때 알림을 발송 가능

      1. 시그널 정의
      • Django의 Signal 클래스를 사용하여 사용자 정의 시그널을 정의 가능

        from django.dispatch import Signal
        
        # 사용자 정의 시그널 정의
        my_custom_signal = Signal(providing_args=["arg1", "arg2"])
      1. 수신기 연결
      • 사용자 정의 시그널과 수신기 함수를 연결
      from django.dispatch import receiver
      
      @receiver(my_custom_signal)
      def handle_my_custom_signal(sender, **kwargs):
          print("사용자 정의 시그널이 호출되었습니다!")
          print(f"인자: {kwargs}")
      1. 시그널 발송
      • 특정 이벤트가 발생했을 때 시그널을 발송
      # 시그널 발송
      my_custom_signal.send(sender=None, arg1="Hello", arg2="World")

시그널의 예시

1. 사용자 생성 후 프로필 생성

  • ex. 사용자가 회원가입할 때, 자동으로 프로필을 생성하는 시나리오
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from myapp.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

2. 모델 삭제 전후 처리

  • 모델이 삭제되기 전 또는 후에 특정 작업을 수행 가능
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(pre_delete, sender=MyModel)
def before_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제되기 전입니다.")

@receiver(post_delete, sender=MyModel)
def after_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제된 후입니다.")

시그널의 장점과 단점

장점

  • 중앙 집중화된 이벤트 처리: 특정 이벤트가 발생할 때마다 다른 곳에서 로직을 실행 가능
  • 코드의 분리: 모델의 로직과 추가 작업을 분리하여 코드의 응집도를 높일 수 있음
  • 확장성: 추가 로직을 별도의 파일에 두어 유지보수와 확장이 용이

단점

  • 디버깅 어려움: 시그널은 이벤트 기반이기 때문에 디버깅이 어려울 수 있음
  • 복잡성 증가: 여러 시그널이 동시에 작동할 때 로직의 흐름을 파악하기 어려울 수 있음
  • 성능 저하: 대규모 애플리케이션에서 시그널이 과도하게 사용되면 성능 문제가 발생 가능

day 5 😋

현재까지의 구조 리뷰 / 코드 리뷰


현재까지의 구조

moneybook/
├── .venv/
│
├── accounts/
│   ├── migrations/
│   ├── admin.py
│   ├── apps.py
│   ├── constants.py
│   ├── forms.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
│
├── analysis/
│   ├── migrations/
│   ├── admin.py
│   ├── analyzers.py
│   ├── apps.py
│   ├── models.py
│   ├── serializers.py
│   ├── tasks.py
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py
│   ├── views.py
│   └── views_html.py
│
├── core/
│   ├── __init__.py
│   ├── asgi.py
│   ├── celery.py
│   ├── settings.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
│   
├── docker/
├── media/
├── static/
├── templates/
│
├── transactions/
│   ├── migrations/
│   ├── admin.py
│   ├── apps.py
│   ├── filters.py
│   ├── forms.py
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   ├── views.py
│   └── views_html.py
│
├── users/
│   ├── migrations/
│   │   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
│
├── .gitignore
├── __init__.py
├── celerybeat-schedule
├── db.sqlite3
├── manage.py
├── poetry.lock
├── pyproject.toml
└── README.md

전체 구조의 특징

  • Django 앱 4개

    • accounts / transactions / analysis / users

  • core 프로젝트 설정

    • settings.py / urls.py / celery.py


accounts

├── accounts/
│   ├── migrations/ - DB 테이블 변경 내역을 기록하는 폴더(절대 직접 수정X)
│   ├── admin.py - Django Admin 페이지에서 모델을 어떻게 표시할지 설정하는 곳
│   ├── apps.py - Django가 이 앱을 인식하도록 하는 메타데이터 설정 파일
│   ├── constants.py - 앱 내부에서 사용하는 상수(코드, 선택지, ENUM 등) 정리하는 파일
│   ├── forms.py - Django Form / ModelForm을 정의하는 파일
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py - 실제 기능(비즈니스 로직)을 수행하는 코드가 들어가는 곳

models.py

class Account(models.Model):
    user = models.ForeignKey(
        User,on_delete=models.CASCADE,related_name="accounts",
    )

    bank_code = models.CharField(
        max_length=3,choices=BANK_CODES, verbose_name="은행",
    )

    account_number = models.CharField(
        max_length=30,verbose_name="계좌번호",
    )

    account_type = models.CharField(
        max_length=30,choices=ACCOUNT_TYPE,verbose_name="계좌 종류",
    )

    name = models.CharField(
        max_length=50,verbose_name="계좌/지갑 이름",
    )

    created_at = models.DateTimeField(
        auto_now_add=True,verbose_name="생성일",
    )

    def __str__(self):
        return f"[{self.name}]{self.get_bank_code_display()} -
        {self.get_account_type_display()}"

    • ForeignKey에서 역참조 이름을 지정하는 옵션
      • User → Account 관계에서 user.accounts.all() 로 해당 유저 계좌 목록을 조회 가능해짐
      • 미사용시, 기본값이 account_set 이라서, user.account_set.all()
    • 즉, 유저 객체에서 user.accountsAccount 목록 조회 가능하도록 설정
    • 나중에 모델 이름이 바뀌어도 직관적인 이름 그대로 사용 가능
    • 하나의 모델에서 같은 모델을 2개 이상 참조할 때 필수(안쓰면 오류 발생)
  • verbose_name="은행"

    • 관리자(admin) 페이지나 폼(Form) 출력 시 표시되는 필드의 한글 라벨을 지정하는 역할
    • admin 페이지에서 이렇게 보임: | 은행 | (입력란) |

forms.py

  • HTML Form을 자동으로 만들어주는 Django ModelForm
from django import forms
from .models import Account

class AccountForm(forms.ModelForm):
    class Meta:
        model = Account
        fields = ["bank_code", "account_number", "account_type", "name"]
        widgets = {
            "bank_code": forms.Select(),
            "account_type": forms.Select(),
            "account_number": forms.TextInput(attrs={"placeholder": "계좌번호"}),
            "name": forms.TextInput(attrs={"placeholder": "계좌별명"})
        }

  • forms.ModelForm
    • Django의 ModelForm을 상속한 폼 클래스.
    • ModelForm은 특정 모델을 기반으로 자동으로 HTML Form을 생성해줌
    • 즉, Account 모델을 등록/수정할 때 사용하는 입력폼을 만들기 위한 클래스
  • class Meta:
    • ModelForm의 내부 설정값을 정의하는 부분.
  • model = Account
    • 이 Form이 어떤 모델을 기반으로 만들어질지 결정하는 설정.
  • fields = []
    • 이 Form에서 어떤 필드를 입력받을지 지정
  • widgets = { ... }
    • 각 필드가 HTML에서 어떤 위젯으로 표시될지 정의하는 부분.

  • "bank_code": forms.Select()

<select name="bank_code">
    <option value="KB">국민은행</option>
    <option value="NH">농협</option>
</select>
  • "account_number": forms.TextInput(attrs={"placeholder": "계좌번호"})
    • <input type="text"> 로 렌더링됨
<input type="text" name="account_number" placeholder="계좌번호">
  • "name": forms.TextInput(attrs={"placeholder": "계좌별명"})

<input type="text" name="name" placeholder="계좌별명">

  • 실제 렌더링 예시

    • 렌더링 결과
      • 은행 선택 Select 박스 / 계좌번호 입력 / 계좌 타입 Select 박스 / 계좌 별명 입력
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit" class="btn btn-dark">등록</button>
</form>

views.py

from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect, get_object_or_404
from .models import Account
from .forms import AccountForm


@login_required
def account_list_view(request):
    accounts = Account.objects.filter(user=request.user).order_by("created_at")
    return render(request, "accounts/account_list.html", {
        "accounts": accounts,
    })


@login_required
def account_create_view(request):
    if request.method == "POST":
        form = AccountForm(request.POST)
        if form.is_valid():
            account = form.save(commit=False)
            account.user = request.user
            account.save()
            return redirect("accounts:account_list")
    else:
        form = AccountForm()
    return render(request, "accounts/account_form.html", {
        "form": form,
    })


@login_required
def account_delete_view(request, pk):
    account = get_object_or_404(Account, pk=pk, user=request.user)

    if request.method == "POST":
        account.delete()
        return redirect("accounts:account_list")

    return redirect("accounts:account_list")

  • render(request, template_name, context=None)
    • Django에서 HTML 템플릿을 불러오고,
    • 데이터(context)를 전달해서 최종 HTML을 만들어 반환하는 함수
      • 즉, 백엔드에서 데이터를 가져와 HTML로 변환해 사용자 브라우저로 보내는 역할
  • request
    • 요청을 보낸 사용자 정보 / 로그인 여부 / 요청 방식(GET/POST) / form 데이터 / 세션 정보
    • 등이 담겨있음
  • "accounts/account_list.html"
    • 두 번째 인자 → 렌더링할 HTML 템플릿 경로
    • Django는 templates/ 폴더에서 찾아 들어감 -> accounts/account_list.html 파일을 렌더링
  • { "accounts": accounts }
    • 템플릿에 전달할 데이터(Context)
    • 템플릿 파일에서 accounts 라는 이름으로 이 데이터를 사용 가능.(아래처럼 사용 가능)
{% for a in accounts %}
  <p>{{ a.name }}</p>
{% endfor %}

  • render가 하는일 (요약)
    • accounts/account_list.html 파일을 찾음
    • "accounts"라는 이름으로 accounts QuerySet을 템플릿에 전달
    • 템플릿에서 데이터가 삽입된 최종 HTML 생성
    • 그 HTML을 브라우저로 응답(Response) 형태로 반환
      • “템플릿 + 데이터 → 완성된 HTML → 사용자에게 보여줌”

urls.py

from django.urls import path
from . import views  # views 모듈 전체를 불러오기

app_name = "accounts"

urlpatterns = [
    path("", views.account_list_view, name="account_list"),
    path("create/", views.account_create_view, name="account_create"),
    path("<int:pk>/delete/", views.account_delete_view, name="account_delete"),
]

  • name를 붙이는 이유?
      1. URL을 직접 쓰지 않고 이름으로 역참조(reverse)할 수 있음
      • <a href="/accounts/create/">계좌 생성</a> 이렇게 사용시
        • 나중에 URL을 변경하면 모든 템플릿을 고쳐야 함
      • <a href="{% url 'accounts:account_create' %}">계좌 생성</a>
        • URL이 바뀌어도 name은 그대로이기 때문에 아무것도 고칠 필요 없음.
      1. reverse() / redirect() 에서 사용 가능
      • return redirect("/accounts/") -> 직접 작성 -> 불편
      • return redirect("accounts:account_list") -> name로 역참조
      1. 대규모 프로젝트에서 URL 충돌을 방지 (namespace 사용)
      • app_name = "accounts" 이렇게 네임스페이스를 붙여두면 다른 앱과 이름이 겹쳐도 충돌X
      • accounts:account_list / transactions:account_list = 충돌X
      1. HTML에서 URL 하드코딩이 필요 없어짐
      • <a href="/accounts/1/delete/">삭제</a>
      • <a href="{% url 'accounts:account_delete' pk=account.id %}">삭제</a>

analysis

구조

├── analysis/
│   ├── migrations/
│   ├── admin.py
│   ├── analyzers.py - 분석 그래프 생성 / Pandas·Matplotlib 로직 담당
│   ├── apps.py
│   ├── models.py
│   ├── serializers.py - DRF용 직렬화 파일 → API로 반환할 데이터를 정의
│   ├── tasks.py - Celery 비동기 작업(Task) 정의
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py - 날짜 범위 계산 등의 재사용되는 유틸 함수 모음
│   ├── views.py - DRF APIView / GenericAPIView 기반의 분석 API (JSON 응답)
│   └── views_html.py - HTML 화면을 렌더링하는 View (render 사용)

views.py / views_html.py

  • views.py / views_html.py 분리하는 이유

    • 작은 프로젝트인 경우 views.py 하나에 전부 넣어도 문제 없음
    • HTML 화면(View)과 API(View)를 분리하면 유지보수·가독성·확장성에서 압도적으로 유리
    1. API 뷰와 HTML 뷰는 성격이 완전히 다르기 때문

      구분HTML ViewAPI View
      반환HTML 템플릿JSON
      목적사용자 브라우저 UI프론트/모바일/JS 통신
      특징render 사용Response(JSON) 사용
      권한로그인 기반 많음JWT·Token 기반 많음
      URL 패턴UI 중심REST API 규칙 중심
    1. FE/BE 분리 개발에 유리
    • 나중에 Nuxt/React/Vue 같은 프런트엔드 앱을 붙이게 되면
      • views.py = API / views_html.py = 웹 브라우저 화면
    1. DRF(API)와 Django Template(HTML)은 아예 다른 스타일
    • HTML 방식: return render(request, "analysis/create.html", ctx)
    • API 방식: return Response(serializer.data)

view.py

  • 조회 (ListAPIView)

    • ListAPIView(DRF의 제네릭 뷰)
      • 자동으로 serializer 적용 / 목록 조회 전용 / 자동으로 GET 요청 처리
class AnalysisListView(ListAPIView):
    serializer_class = AnalysisSerializer
    - 1. 응답을 직렬화할 때 사용
    - 1-1. Analysis 모델 데이터를 JSON 형태로 변환할 때 사용할 serializer
    
    permission_classes = [IsAuthenticated]
	- 2. 로그인한 사용자만 조회가능 -> API 토큰/세션 인증 필요
    
    def get_queryset(self):
    - 3. 어떤 데이터를 보여줄지 결정
    - 3-1. ListAPIView는 내부에서 자동으로 get_queryset()을 호출해서 데이터를 가져온다.
    
        user = self.request.user
        qs = Analysis.objects.filter(user=user).order_by("-created_at")
        - 4. Analysis = Django 모델(Model) 
        - 4-1. .objects.filter(user=user) 
          - 로그인한 사용자(= request.user)가 생성한 Analysis 데이터만 가져와

        period = self.request.query_params.get("period")
        - 5. query_params는 Django REST Framework(DRF)에서 
          - GET 요청의 쿼리스트링(parameters)을 가져올 때 사용하는 객체

        if period == "DAILY":
            start, end = get_daily_range()
            qs = qs.filter(period_start=start, period_end=end)

        elif period == "LAST_DAY":
            start, end = get_last_day_range()
            qs = qs.filter(period_start=start, period_end=end)

        .
        .
        .

        return qs
  • 생성 (APIView)

    • create 과정이 단순 serializer.save()가 아니라 분석 로직(Analyzer.run()) 이 필요하기 때문
      • 단순 CRUD가 아니므로 GenericAPIView보다 APIView가 적합
class AnalysisCreateView(APIView):
    permission_classes = [IsAuthenticated]
    
    - 1. 생성 요청 처리
    def post(self, request):  
    
    - 2. 클라이언트가 보낸 정보 읽기.
		user = request.user
        analysis_type = request.data.get("type")
        about = request.data.get("about", "TOTAL_SPENDING")
        description = request.data.get("description", "")


        # 기간 계산
        if analysis_type == "DAILY":
            start_date, end_date = get_daily_range()
        elif analysis_type == "LAST_DAY":
            start_date, end_date = get_last_day_range()
        elif analysis_type == "WEEKLY":
            start_date, end_date = get_week_range()
        elif analysis_type == "LAST_WEEK":
            start_date, end_date = get_last_week_range()
        elif analysis_type == "MONTHLY":
            start_date, end_date = get_month_range()
        elif analysis_type == "LAST_MONTH":
            start_date, end_date = get_last_month_range()
        elif analysis_type == "YEARLY":
            start_date, end_date = get_year_range()
        elif analysis_type == "LAST_YEAR":
            start_date, end_date = get_last_year_range()
        else:
            return Response({"error": "Invalid type"}, status=400)

		- 3. Analyzer 객체 생성 -> 분석기 생성
        analyzer = Analyzer(
            user=user,
            about=about,
            period_type=analysis_type,
            start_date=start_date,
            end_date=end_date,
            description=description,
        )
		
        - 4. 분석 실행
          - run() 결과는 DB에 저장된 Analysis 객체
        result = analyzer.run()
		
        - 5. 생성된 분석 결과를 JSON으로 반환
        return Response({
            "id": result.id,
            "type": result.type,
            "about": result.about,
            "period_start": result.period_start,
            "period_end": result.period_end,
            "image_url": result.result_image.url if result.result_image else None,
        })

analyzers.py

import matplotlib
matplotlib.use("Agg")
  • Agg

    • 서버 환경에서 그래프를 화면에 띄우지 않고 이미지 파일로 생성만 하기 위해 사용하는 필수 설정
    • 웹 서버(Uvicorn, Gunicorn 등)에서는 GUI가 없어서 Agg 백엔드를 지정해야 오류가 안 남

  • import / from

import os
import uuid
import pandas as pd
import matplotlib.pyplot as plt
from django.conf import settings

class Analyzer:

	- 1. init : 초기 설정 = 분석 요청 시 전달되는 메타정보 저장 ⭐️
    def __init__(self, user, about, period_type, start_date, end_date, description=""):
        self.user = user
        self.about = about
        self.period_type = period_type
        self.start_date = start_date
        self.end_date = end_date
        self.description = description

        - 2. MEDIA_ROOT/analysis/유저ID/ 경로에 저장.
        self.output_dir = os.path.join(settings.MEDIA_ROOT, "analysis", str(user.id))
        - 2-1. 각 사용자별 폴더를 자동 생성
        os.makedirs(self.output_dir, exist_ok=True)
	
    - 3. DB에서 해당 기간 데이터 조회 ⭐️
    def get_transactions(self):
        from transactions.models import Transaction

        qs = Transaction.objects.filter(
            user=self.user,
            transacted_at__date__gte=self.start_date,
            transacted_at__date__lte=self.end_date,
        ).values("transacted_at", "amount", "type", "category")
        - 4. .values() 를 사용해 QuerySet → 단순 dict 리스트로 가져옴
        - 4-1. pandas가 바로 DataFrame으로 변환 가능하게 하기 위함

        return pd.DataFrame(qs)
		- 5. DataFrame 형태로 바로 분석할 수 있게 반환
        
    - 6. 날짜 변환 처리 ⭐️    
    def build_dataframe(self):
        df = self.get_transactions()

        if df.empty:
            return df
		
        - 7. 날짜 비교 & 그래프 등을 위해 datetime 타입으로 변환.
        df["transacted_at"] = pd.to_datetime(df["transacted_at"])
        return df

    - 7. 수입/지출/차액 계산 ⭐️
    def calculate_totals(self, df):
    	- 8. type이 “deposit”(입금)인 row들의 amount 합
        total_income = df[df["type"] == "deposit"]["amount"].sum()
        - 9. type이 “withdraw”(출금)인 row들의 amount 합
        total_spending = df[df["type"] == "withdraw"]["amount"].sum()
        - 10. 차액 계산 = 수입 - 지출
        difference = total_income - total_spending
        return total_income, total_spending, difference

    - 11. 새 그래프: 수입 vs 지출 비교 ⭐️⭐️
    def build_plot(self, total_income, total_spending):
        - 12. 그래프 크기 설정
        plt.figure(figsize=(6, 4)) 
		
        labels = ["Income", "Spending"]
        values = [total_income, total_spending]

        - 13. 단순 바(bar) 그래프 형태 / 수입은 초록, 지출은 빨강
        plt.bar(labels, values, color=["green", "red"])
        
        - 14. 제목, y축 표시, 여백 자동 조정.
        plt.title("Income vs Spending")
        plt.ylabel("Amount")
        plt.tight_layout()
	
    15. 그래프 이미지로 저장 ⭐️
    def save_plot_as_image(self):
    
    	- 16. uuid4()를 사용하여 충돌 없는 파일 이름 생성
        filename = f"{uuid.uuid4()}.png"
        filepath = os.path.join(self.output_dir, filename)
		
        - 17. 이미지 파일로 저장 / 메모리에서 그래프 객체 제거
        plt.savefig(filepath)
        plt.close()

        return f"analysis/{self.user.id}/{filename}"

    - 18. Analysis 모델 생성(DB 저장)
    def save_analysis_model(self, image_path, total_income, total_spending, difference):
        from .models import Analysis
        analysis = Analysis.objects.create(
            user=self.user,
            about=self.about,
            type=self.period_type,
            period_start=self.start_date,
            period_end=self.end_date,
            description=self.description,
            result_image=image_path,

            total_income=total_income,
            total_spending=total_spending,
            difference=difference,
        )
        return analysis

	- 19. 전체 프로세스를 순서대로 진행 ⭐️⭐️⭐️
    def run(self):
        df = self.build_dataframe()
        if df.empty:
            raise ValueError("해당 기간에 분석 가능한 거래가 없습니다.")

        # 수입/지출/차액 계산
        total_income, total_spending, difference = self.calculate_totals(df)

        # 그래프 생성
        self.build_plot(total_income, total_spending)

        # 그래프 저장
        image_path = self.save_plot_as_image()

        # 모델 저장 (수치 포함)
        return self.save_analysis_model(image_path, total_income, total_spending, difference)
  • 전체 흐름 정리

run()
 └─ build_dataframe()
      └─ get_transactions()  → DataFrame
 └─ calculate_totals()       → income/spending/difference
 └─ build_plot()             → 그래프 생성
 └─ save_plot_as_image()     → 이미지 저장, 경로 리턴
 └─ save_analysis_model()    → DB 저장
 └─ 최종 Analysis 객체 반환

urls.py

from django.urls import path
from .views import AnalysisListView, AnalysisCreateView
from .views_html import analysis_list_html, analysis_create_html

app_name = "analysis"

urlpatterns = [
    # API
    path("api/", AnalysisListView.as_view(), name="analysis_list"),
    path("api/create/", AnalysisCreateView.as_view(), name="analysis_create"),

    # HTML
    path("html/", analysis_list_html, name="analysis_html"),
    path("html/create/", analysis_create_html, name="analysis_create_html"),
]
  • AnalysisListView.as_view() / analysis_list_html

    • Django에서 CBV 와 FBV 를 URL에 연결하는 방식 차이를 보여주는 코드
코드의미
AnalysisListView.as_view()클래스 기반 뷰(CBV)를 URL에 연결하는 방식
analysis_list_html함수 기반 뷰(FBV)를 URL에 연결하는 방식
  • AnalysisListView.as_view() — 클래스 기반 뷰(CBV)

    • CBV: 뷰를 클래스로 작성하는 방식 (REST APIView, ListAPIView 등)
    • class AnalysisListView(ListAPIView):
      • 클래스이기 때문에 그대로 URL에 연결하면 동작하지 않는다
      • 아래처럼 클래스를 "뷰 함수"로 바꿔주는 메서드가 필요
    • AnalysisListView.as_view()
  • .as_view()

    • 클래스를 실행 가능한 함수(view 함수)로 변환
    • HTTP 메서드(get/post/put/delete)를 자동으로 매핑
    • DRF/제네릭뷰 기능을 모두 초기화
  • analysis_list_html — 함수 기반 뷰(FBV)

def analysis_list_html(request):
    return render(request, "analysis/list.html")
  • 그냥 함수이기 때문에 url에 그대로 넣으면 바로 동작

serializers.py

  • DateTimeField → DateTimeFieldSerializer
    • ForeignKey → PrimaryKeyRelatedField
      • ImageField → URL 형태로 출력 -> 직접 하나하나 serializer 필드를 만들 필요 없음
  • 유효성 검사 자동 지원
    • name = models.CharField(max_length=50)
    • DRF가 자동으로: 비어있는지 체크 / max_length 체크 / 타입 검사
from rest_framework import serializers
from .models import Analysis

class AnalysisSerializer(serializers.ModelSerializer):
    class Meta:
        model = Analysis
        fields = "__all__"
  • class AnalysisSerializer(serializers.ModelSerializer):
    • ModelSerializer: DRF에서 모델을 기반으로 자동으로 Serializer를 생성해주는 클래스.
      • 즉, Django 모델을 JSON 형태로 변환
      • 클라이언트로부터 받은 데이터를 모델 객체로 변환해주는 역할을 함.
  • class Meta:
    • "이 Serializer가 어떤 모델을 변환할지", "어떤 필드를 노출시킬지" 설정하는 부분
  • model = Analysis
    • 이 Serializer가 처리할 대상 모델은 Analysis 모델이라는 뜻
    • Analysis 객체 → JSON 변환 가능 / JSON 입력 → Analysis 객체 생성 가능
  • fields = "__all__"
    • Analysis 모델의 모든 필드를 JSON으로 변환하겠다는 뜻
    • 실무에서는 보통 read_only_fields = ["id", "created_at"] 이런식으로 수정불가 처리
      • id / created_at (auto_now_add=True) / updated_at (auto_now=True)
      • user (로그인된 유저로 강제 설정) / balance_after (거래 등록 시 자동 계산)
      • period_start / period_end (분석 생성 시 서버에서 계산) 이런 필드 보통 넣음
# 모델 필드
id
user
about
period_type
period_start
period_end
result_image
created_at

# JSON 응답
{
    "id": 1,
    "user": 3,
    "about": "TOTAL_SPENDING",
    "period_type": "MONTHLY",
    "period_start": "2025-11-01",
    "period_end": "2025-11-30",
    "result_image": "/media/analysis/3/graph_2025_11.png",
    "created_at": "2025-11-27T13:45:00"
}

day 6 🤪

Django Signal / Django JWT


Django Signal

  • Django는 특정 이벤트가 발생하였을 때
    • 다른 수신기에 시그널을 보내 특정 동작을 수행할 수 있도록 해주는 Signal 객체가 구현되어 있음
    • 이를 활용하면 특정 모델이 생성되었을때 / 특정 api에 요청이 들어왔을 때 등의 상황에서
      • 해당 상황에 맞는 동작을 구현할 수 있음

  • 정의

    • 특정 이벤트가 발생할 때 다른 부분에서 알림을 받아 추가 작업을 수행할 수 있도록 하는 기능

      • 이 시그널은 이벤트 기반 프로그래밍의 한 형태로
        • 모델의 저장, 삭제 등의 중요한 작업이 발생할 때 추가적인 로직을 실행할 수 있음
    • 시그널을 사용 ex.

      • 모델이 저장되거나 삭제될 때 이를 감지하여 다른 작업을 트리거
      • Django는 이런 이벤트들을 처리하기 위해 여러 내장 시그널을 제공하며,
        • 사용자 정의 시그널을 만들 수도 있음

  • 주요 개념

    • 1. 시그널 (Signal)

      • 시그널은 특정 이벤트가 발생했을 때 발송
      • 예를 들어, 모델의 인스턴스가 저장될 때 시그널이 발송될 수 있음
      • Django는 다양한 내장 시그널을 제공하며, 사용자는 이를 직접 정의할 수도 있음
    • 2. 수신기 (Receiver)

      • 수신기는 시그널을 받아 처리하는 함수
      • 시그널이 발송될 때 이를 수신하여 특정 동작을 수행하는 역할
      • 일반적으로 시그널을 정의하고 나면, 이 시그널에 수신기를 등록하여 시그널을 처리함
    • 3. 발송자 (Sender)

      • 시그널이 발송될 때, 어떤 객체나 클래스가 시그널을 발송했는지를 나타냄
      • 시그널을 특정 발송자와 연결하여, 그 발송자에서 발생하는 이벤트에 대해서만 시그널을 수신 가능

  • Django 내장 시그널

    • Django는 몇 가지 내장 시그널을 제공 / 아래의 예시는 가장 자주 사용되는 시그널
        1. pre_save: 모델의 인스턴스가 저장되기 에 호출
        1. post_save: 모델의 인스턴스가 저장된 에 호출
        1. pre_delete: 모델의 인스턴스가 삭제되기 에 호출
        1. post_delete: 모델의 인스턴스가 삭제된 에 호출
        1. m2m_changed: 다대다 관계가 변경될 때 호출
        1. request_started: HTTP 요청이 시작될 때 호출
        1. request_finished: HTTP 요청이 끝났을 때 호출

signal 사용 방법

  • 1. 시그널 정의 및 수신기 연결

    • 시그널을 사용하기 위해서는 수신기(Receiver)를 정의 및 이 수신기를 특정 시그널에 연결 필요
    • Django에서 시그널을 정의할 필요는 없음
      • 내장 시그널을 사용하거나 필요에 따라 사용자 정의 시그널을 만들 수 있음
  • 2. 수신기 함수 작성

from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 수신기 함수 정의
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    if created:
        print(f"{instance}이(가) 생성되었습니다!")
    else:
        print(f"{instance}이(가) 수정되었습니다!")
  • 수신기 함수는 시그널이 발송될 때 호출되는 함수

    • 수신기 함수는 반드시 두 개의 인자를 받아야 함 -> sender / **kwargs
  • 3. 시그널과 수신기 연결

from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 시그널과 수신기 연결
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    # 저장된 후 처리할 로직
    if created:
        print(f"새 인스턴스 {instance}가 생성되었습니다!")
  • 시그널과 수신기를 연결하기 위해 @receiver 데코레이터를 사용할 수 있음
    • 이 데코레이터는 특정 시그널이 발송될 때 해당 수신기 함수를 실행하도록 설정함
    • 또는 signals.connect() 메서드를 사용해 시그널과 수신기를 명시적으로 연결도 가능

사용자 정의 시그널

  • 내장 시그널 외에도 사용자 정의 시그널을 만들어 특정 이벤트가 발생했을 때 알림을 발송할 수 있다.

    1. 시그널 정의

    • Django의 Signal 클래스를 사용하여 사용자 정의 시그널을 정의 가능
from django.dispatch import Signal

# 사용자 정의 시그널 정의
my_custom_signal = Signal(providing_args=["arg1", "arg2"])
  • 2. 수신기 연결

    • 사용자 정의 시그널과 수신기 함수를 연결
from django.dispatch import receiver

@receiver(my_custom_signal)
def handle_my_custom_signal(sender, **kwargs):
    print("사용자 정의 시그널이 호출되었습니다!")
    print(f"인자: {kwargs}")
  • 3. 시그널 발송

    • 특정 이벤트가 발생했을 때 시그널을 발송
# 시그널 발송
my_custom_signal.send(sender=None, arg1="Hello", arg2="World")

시그널의 예시

  • 1. 사용자 생성 후 프로필 생성

    • 사용자가 회원가입 할 때, 자동으로 프로필을 생성하는 시나리오 가능
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from myapp.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
  • 2. 모델 삭제 전후 처리

    • 모델이 삭제되기 전 또는 후에 특정 작업을 수행 가능
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(pre_delete, sender=MyModel)
def before_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제되기 전입니다.")

@receiver(post_delete, sender=MyModel)
def after_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제된 후입니다.")

시그널의 장점과 단점

  • 장점

    • 중앙 집중화된 이벤트 처리: 특정 이벤트가 발생할 때마다 다른 곳에서 로직을 실행할 수 있음
    • 코드의 분리: 모델의 로직과 추가 작업을 분리하여 코드의 응집도를 높일 수 있음
    • 확장성: 추가 로직을 별도의 파일에 두어 유지보수와 확장이 용이
  • 단점

    • 디버깅 어려움: 시그널은 이벤트 기반이기 때문에 디버깅이 어려울 수 있음
    • 복잡성 증가: 여러 시그널이 동시에 작동할 때 로직의 흐름을 파악하기 어려울 수 있음
    • 성능 저하: 대규모 애플리케이션에서 시그널이 과도하게 사용되면 성능 문제가 발생 가능

Django JWT


사용법

  • poetry add djangorestframework-simplejwt

  • JWT를 이용한 회원가입 기능 구현



day 7

분석 기능 코드 단순화


profile
안녕하세요.

0개의 댓글