
디렉토리별 간단 설명 / 파일 상세 설명 / 필터
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/settings.pycore/urls.pyaccounts/ , transactions/ 앱 URL을 include 해서 연결core/wsgi.pycore/asgi.pyaccounts/models.pyaccounts/serializers.pyfields → id, bank_name, account_number, balance.accounts/views.pyAccountListCreateView → 로그인 유저의 계좌 목록 조회 & 생성AccountDetailView → 한 계좌 조회·수정·삭제accounts/urls.py/accounts/ 경로에 대해:transactions/models.pytransactions/serializers.pyread_only: user, created_at, updated_at transactions/views.pyTransactionListCreateViewTransactionDetailViewtransactions/urls.py/transactions/ 경로 처리:<pk>/ → 단건 조회·수정·삭제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 네임스페이스 설정{% url app_name:name %} 처럼 사용path('', AccountListCreateView.as_view(), name='account_list')/accounts/ → 계좌 목록 조회 & 생성path('<int:pk>/', AccountDetailView.as_view(), name='account_detail') /accounts/<pk>/ → 계좌 상세 조회/수정/삭제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 데이터를 직렬화하는 Serializerclass AccountDetailView(RetrieveUpdateDestroyAPIView):
serializer_class = AccountSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Account.objects.filter(user=self.request.user) # 본인의 계좌만 접근 허용
RetrieveUpdateDestroyAPIView → 단일 계좌 조회·수정·삭제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 → 응답/요청에 포함할 필드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}"
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 / PermissionDeniedclass TransactionListCreateView(ListCreateAPIView):
serializer_class = TransactionSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['category']
ordering_fields = ['transacted_at', 'amount']
ListCreateAPIView → 거래 조회 + 생성 APIfilter_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 → 단일 거래 조회·수정·삭제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>/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 → 수정 불가 필드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}"
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]
http://127.0.0.1:8000/accounts/http://127.0.0.1:8000/accounts/1/
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-01http://127.0.0.1:8000/transactions/?end=2025-01-31http://127.0.0.1:8000/transactions/?start=2025-01-01&end=2025-01-31
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=-amountpoetry add drf-yasgsettings.py 설정 → INSTALLED_APPS → 'drf_yasg'core/urls.py 수정
http://127.0.0.1:8000/swagger/ / http://127.0.0.1:8000/redoc/
FilterSet 적용시 이점코드 리뷰 / 부가 기능
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()
def perform_destroy(self, instance):
account = instance.account
instance.delete()
# 삭제 후 전체 balance_after 다시 계산
recalc_account_transactions(account)
def perform_update(self, serializer):
instance = self.get_object()
account = instance.account
# 새 값으로 저장
serializer.save()
# 수정 후 전체 거래 balance_after 재계산
recalc_account_transactions(account)
path('', AccountListCreateView.as_view(), name='account_list')path('<int:pk>/', AccountDetailView.as_view(), name='account_detail')AccountListCreateViewListCreateAPIView → GET(목록), POST(생성)IsAuthenticated → 인증 필요 get_queryset() → 로그인한 사용자 본인의 계좌만 조회perform_create() → 계좌 생성 시 자동으로 user 설정AccountDetailViewRetrieveUpdateDestroyAPIView → GET/PUT/PATCH/DELETE 가능/transactions/ → 목록 조회 + 생성/transactions/3/ → 조회 + 수정 + 삭제 def perform_create(self, serializer):
account = serializer.validated_data['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()
)
last_balance = last_transaction.balance_after if last_transaction else 0
amount = serializer.validated_data['amount']
type = serializer.validated_data['type']
# 새 잔액 계산
if type == 'INCOME':
new_balance = last_balance + amount
else:
new_balance = last_balance - amount
# 저장
serializer.save(
user=self.request.user,
balance_after=new_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'),
Pandas / Matplotlib / Celery / 코드리뷰
참고 문서
Pandas, Matplotlib, Celery 를 이용해서 매주, 매월 소비습관 분석하기Matplotlib, PandasPandas = 데이터를 정리, 분석 / Matplotlib = 데이터를 그래프로 시각화Celery데이터를 다루기 위한 파워풀한 도구 — “엑셀을 파이썬으로 더 강력하게 쓰는 느낌”
# 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()
데이터를 그래프로 그려주는 라이브러리 — Pandas와 세트로 자주 쓰임
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()
# 월별 지출 그래프
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()
[클라이언트] → Django API → (Celery에게 작업 전달) → 바로 응답
↓
[Celery Worker]
(= 백그라운드 작업 처리)
Django + DRF 개발할 때 무거운 작업을 from celery import shared_task
import time
@shared_task
def slow_task():
time.sleep(10)
print("10초 작업 완료")
# Django에서 호출
## → Django는 바로 응답하고, Celery가 백그라운드에서 실행
slow_task.delay()
Redis(또는 RabbitMQ) 는 거의 필수(보통 Redis를 가장 많이 사용)Redis 가 가장 간단하게 해주기 때문Django (작업 요청)
↓
[메시지 큐] ← Redis or RabbitMQ
↓
Celery Worker (작업자)
Django ↔ Celery worker 사이의 “작업 전달”을 담당하는 게 Redis임.

1. Analysis 모델을 JSON 변환/검증하기 위한 Serializer 선언.
class AnalysisSerializer(serializers.ModelSerializer):
2. Analysis 모델의 모든 필드를 포함하도록 설정
class Meta:
model = Analysis
fields = "__all__"
@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 저장 + 이미지 저장
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
- 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,
})
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"),
]
import matplotlib
matplotlib.use("Agg")
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 객체 반환
필터 html / Celery / Django Signal





DRFView 에는 구현되어 있으나 HTMLView 에는 구현이 안되있는 상태# 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>
비동기 작업 및 작업 대기열을 관리하는 Python 기반의 분산 메시지 큐 시스템
주로 백그라운드 작업 처리, 비동기 작업 처리, 그리고 스케줄링된 작업을 처리하는 데 사용
Celery는 분산 시스템이기 때문에 여러 노드에서 동시에 작업을 처리할 수 있어 확장성이 뛰어나며,
celery beat라는 스케줄러가 필요Redis나 RabbitMQ가 이 역할을 자주 수행from celery.schedules import crontab
app.conf.beat_schedule = {
'add-every-midnight': {
'task': 'tasks.add',
'schedule': crontab(minute=0, hour=0),
'args': (16, 16),
},
}
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
result = add.delay(4, 6)poetry add django-celery-beatdjango-celery-beat 를 설치 불가django-timezone-field 라이브러리가 Python < 4.0 만 지원 -> requires-python = ">=3.13,<4.0" 버전을 명시brew install redisbrew services start redisdocker run -d --name redis -p 6379:6379 redis:7
celery.py 파일을 core/ 에 넣고 __init__.py 에 Celery App 연결한 이유manage.py 와 같은 디렉토리(= settings.py 가 들어있는 디렉토리)에 넣어야 하기 때문os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moneybook.settings")| 구분 | @app.task | @shared_task |
|---|---|---|
| Celery 앱 필요? | 필요함 (app 인스턴스 기반) | 필요 없음 (프로젝트 어디서나 사용 가능) |
| 추천 용도 | 단일 앱 형태의 Celery 구성 | Django의 여러 앱에서 task 를 사용할 때 |
| 장점 | 명시적이고 구조적임 | Django 프로젝트에서 가장 많이 사용됨 |
@shared_task 가 정석analysis/tasks.pyfrom 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}"
Celery worker와 beat 실행하기Worker -> 작업을 실제로 처리하는 사람(일꾼)Beat -> 스케줄러(일정 관리자)celery -A core worker --loglevel=INFOcelery -A core beat --loglevel=INFO
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
})

Django Signal
특정 이벤트가 발생할 때 다른 부분에서 알림을 받아 추가 작업을 수행할 수 있도록 하는 기능
pre_save: 모델의 인스턴스가 저장되기 전에 호출post_save: 모델의 인스턴스가 저장된 후에 호출pre_delete: 모델의 인스턴스가 삭제되기 전에 호출post_delete: 모델의 인스턴스가 삭제된 후에 호출m2m_changed: 다대다 관계가 변경될 때 호출request_started: HTTP 요청이 시작될 때 호출request_finished: HTTP 요청이 끝났을 때 호출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}이(가) 수정되었습니다!")
@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}가 생성되었습니다!")
내장 시그널 외에도 사용자 정의 시그널을 만들어 특정 이벤트가 발생했을 때 알림을 발송 가능
Django의 Signal 클래스를 사용하여 사용자 정의 시그널을 정의 가능
from django.dispatch import Signal
# 사용자 정의 시그널 정의
my_custom_signal = Signal(providing_args=["arg1", "arg2"])
from django.dispatch import receiver
@receiver(my_custom_signal)
def handle_my_custom_signal(sender, **kwargs):
print("사용자 정의 시그널이 호출되었습니다!")
print(f"인자: {kwargs}")
# 시그널 발송
my_custom_signal.send(sender=None, arg1="Hello", arg2="World")
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()
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}이(가) 삭제된 후입니다.")
현재까지의 구조 리뷰 / 코드 리뷰
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
accounts / transactions / analysis / userssettings.py / urls.py / celery.py├── 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 - 실제 기능(비즈니스 로직)을 수행하는 코드가 들어가는 곳
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()}"
related_name="accounts"User → Account 관계에서 user.accounts.all() 로 해당 유저 계좌 목록을 조회 가능해짐account_set 이라서, user.account_set.all()user.accounts 로 Account 목록 조회 가능하도록 설정verbose_name="은행"Django ModelFormfrom 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.ModelFormclass Meta:model = Accountfields = []widgets = { ... }"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="계좌별명">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-dark">등록</button>
</form>
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)request"accounts/account_list.html"{ "accounts": accounts }{% for a in accounts %}
<p>{{ a.name }}</p>
{% endfor %}
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"),
]
<a href="/accounts/create/">계좌 생성</a> 이렇게 사용시 <a href="{% url 'accounts:account_create' %}">계좌 생성</a> <a href="/accounts/1/delete/">삭제</a><a href="{% url 'accounts:account_delete' pk=account.id %}">삭제</a>├── 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 분리하는 이유
API 뷰와 HTML 뷰는 성격이 완전히 다르기 때문
| 구분 | HTML View | API View |
|---|---|---|
| 반환 | HTML 템플릿 | JSON |
| 목적 | 사용자 브라우저 UI | 프론트/모바일/JS 통신 |
| 특징 | render 사용 | Response(JSON) 사용 |
| 권한 | 로그인 기반 많음 | JWT·Token 기반 많음 |
| URL 패턴 | UI 중심 | REST API 규칙 중심 |
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
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,
})
import matplotlib
matplotlib.use("Agg")
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 객체 반환
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() | 클래스 기반 뷰(CBV)를 URL에 연결하는 방식 |
analysis_list_html | 함수 기반 뷰(FBV)를 URL에 연결하는 방식 |
def analysis_list_html(request):
return render(request, "analysis/list.html")
name = models.CharField(max_length=50)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를 생성해주는 클래스.class Meta:model = Analysisfields = "__all__"read_only_fields = ["id", "created_at"] 이런식으로 수정불가 처리# 모델 필드
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"
}
Django Signal / Django JWT
특정 이벤트가 발생할 때 다른 부분에서 알림을 받아 추가 작업을 수행할 수 있도록 하는 기능
pre_save: 모델의 인스턴스가 저장되기 전에 호출post_save: 모델의 인스턴스가 저장된 후에 호출pre_delete: 모델의 인스턴스가 삭제되기 전에 호출post_delete: 모델의 인스턴스가 삭제된 후에 호출m2m_changed: 다대다 관계가 변경될 때 호출request_started: HTTP 요청이 시작될 때 호출request_finished: HTTP 요청이 끝났을 때 호출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 / **kwargsfrom 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() 메서드를 사용해 시그널과 수신기를 명시적으로 연결도 가능내장 시그널 외에도 사용자 정의 시그널을 만들어 특정 이벤트가 발생했을 때 알림을 발송할 수 있다.
Signal 클래스를 사용하여 사용자 정의 시그널을 정의 가능from django.dispatch import Signal
# 사용자 정의 시그널 정의
my_custom_signal = Signal(providing_args=["arg1", "arg2"])
from django.dispatch import receiver
@receiver(my_custom_signal)
def handle_my_custom_signal(sender, **kwargs):
print("사용자 정의 시그널이 호출되었습니다!")
print(f"인자: {kwargs}")
# 시그널 발송
my_custom_signal.send(sender=None, arg1="Hello", arg2="World")
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()
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}이(가) 삭제된 후입니다.")


refresh: 갱신용 / access: access시에 사용가능한 정보


분석 기능 코드 단순화