Front-end
통계영역에 대한 정리
크레비즈
2025. 5. 3. 01:25
상단에 4개의 통계 정보를 카드 형식으로 표현했다.
그 중에 총 사용금액에 대해서만 살펴보면..
<v-col cols="12" sm="6" md="3">
<v-card elevation="2" class="rounded-lg" height="100%">
<v-card-text class="pa-4">
<div class="d-flex align-center mb-2">
<v-avatar :color="getCardColorSync(selectedCardCompany)" size="42" class="mr-3">
<v-icon color="white">mdi-currency-krw</v-icon>
</v-avatar>
<div>
<div class="text-caption text-grey-darken-1 font-weight-medium">총 사용금액</div>
<div class="text-h4 font-weight-bold">{{ formatAmount(totalAmount) }}</div>
</div>
</div>
<v-divider class="my-3"></v-divider>
<div class="d-flex align-center">
<v-icon :color="getCardColorSync(selectedCardCompany)" size="small" class="mr-1">mdi-information</v-icon>
<span class="text-caption text-grey-darken-1">{{ selectedCardCompany || '전체' }} 카드 사용액</span>
</div>
</v-card-text>
</v-card>
</v-col>
총 사용금액 선언 부분이다.
// 통계 데이터
const totalAmount = ref(0); //총사용금액
const topAmountMerchant = ref({ name: '-', amount: 0 });
const topVisitMerchant = ref({ name: '-', count: 0 });
const categoryStats = ref<Record<string, { amount: number, count: number }>>({});
const cardCompanyStats = ref<Record<string, { amount: number, count: number }>>({});
const monthlyStats = ref<Array<{ month: string, amount: number, count: number }>>([]);
이 통계부분은 목록을 조회하고 마지막 부분에 통계 데이터 계산 함수를 넣어두었다.
계산은 back-end 에서 진행하는데 python 으로 코딩되어 있다.
(백엔드는 fastAPI 를 사용했고, 아래 코드 부분은 service 영역(비지니스 영역)이다.
@staticmethod
def get_card_statistics(db: Session, card_code: Optional[str] = None, year: Optional[int] = None, month: Optional[int] = None):
"""
카드 사용내역 통계 조회
Args:
db (Session): 데이터베이스 세션
card_code (str, optional): 카드 코드로 필터링
year (int, optional): 연도로 필터링
month (int, optional): 월로 필터링
Returns:
CardStatistics: 카드 사용내역 통계
"""
# 필터링된 쿼리 생성
query = db.query(Card)
# 카드 코드로 필터링
if card_code:
query = query.filter(Card.card_code == card_code)
# 연도로 필터링
if year:
year_str = str(year)
query = query.filter(Card.transaction_date.like(f'{year_str}%'))
# 월로 필터링
if month:
month_str = str(month).zfill(2) # 1자리 월은 앞에 0 붙임 (1 -> 01)
if year:
# 연도와 월이 모두 있는 경우 (YYYYMM 형식으로 필터링)
year_str = str(year)
query = query.filter(Card.transaction_date.like(f'{year_str}{month_str}%'))
else:
# 월만 있는 경우 (연도 관계없이 해당 월만 필터링)
query = query.filter(Card.transaction_date.like(f'_____{month_str}%'))
# 모든 카드 사용내역 조회 (페이지네이션 없이 전체 데이터)
cards = query.all()
# 총 사용금액 및 건수
total_amount = sum(card.amount for card in cards)
total_count = len(cards)
# 가맹점별 통계 계산
merchant_stats = defaultdict(lambda: {'amount': 0, 'count': 0})
for card in cards:
merchant_stats[card.merchant]['amount'] += card.amount
merchant_stats[card.merchant]['count'] += 1
# 최대 지출 가맹점 찾기
top_amount_merchant = {'name': '-', 'amount': 0, 'count': 0}
for merchant, stats in merchant_stats.items():
if stats['amount'] > top_amount_merchant['amount']:
top_amount_merchant = {
'name': merchant,
'amount': stats['amount'],
'count': stats['count']
}
# 최다 방문 가맹점 찾기
top_visit_merchant = {'name': '-', 'amount': 0, 'count': 0}
for merchant, stats in merchant_stats.items():
if stats['count'] > top_visit_merchant['count']:
top_visit_merchant = {
'name': merchant,
'amount': stats['amount'],
'count': stats['count']
}
# 카테고리별 통계 계산
category_stats = defaultdict(lambda: {'amount': 0, 'count': 0})
for card in cards:
category = card.category or '기타'
category_stats[category]['amount'] += card.amount
category_stats[category]['count'] += 1
# 카드사별 통계 계산
card_company_stats = defaultdict(lambda: {'amount': 0, 'count': 0})
for card in cards:
card_company_stats[card.card_code]['amount'] += card.amount
card_company_stats[card.card_code]['count'] += 1
# 월별 통계 계산 - 최근 12개월 데이터 생성
from datetime import datetime, timedelta
# 현재 날짜 기준으로 최근 12개월 기간 생성
current_date = datetime.now()
months = []
# 현재 월부터 11개월 전까지 추가 (총 12개월)
for i in range(11, -1, -1):
# i개월 전 날짜 계산
past_date = current_date - timedelta(days=i*30) # 대략적인 월 계산
month_key = f"{past_date.year}-{past_date.month:02d}"
months.append(month_key)
# 기본 데이터 초기화 (12개월 전부 0으로 초기화)
monthly_stats = {month: {'amount': 0, 'count': 0} for month in months}
# 실제 데이터로 채우기
for card in cards:
# 거래일자에서 연월 추출 (YYYYMMDD -> YYYY-MM)
if len(card.transaction_date) >= 6:
year = card.transaction_date[:4]
month = card.transaction_date[4:6]
month_key = f"{year}-{month}"
# 최근 12개월 데이터에 포함된 월인 경우에만 추가
if month_key in monthly_stats:
monthly_stats[month_key]['amount'] += card.amount
monthly_stats[month_key]['count'] += 1
# 연월 정렬을 위해 리스트로 변환
monthly_stats_items = [(month, stats) for month, stats in monthly_stats.items()]
# 연월 순으로 정렬
monthly_stats_items.sort(key=lambda x: x[0])
# 결과 변환
category_stats_list = [
CategoryStat(name=category, amount=stats['amount'], count=stats['count'])
for category, stats in category_stats.items()
]
card_company_stats_list = [
CardCompanyStat(name=card_code, amount=stats['amount'], count=stats['count'])
for card_code, stats in card_company_stats.items()
]
monthly_stats_list = [
MonthlyStat(month=month, amount=stats['amount'], count=stats['count'])
for month, stats in monthly_stats_items
]
# 통계 결과 반환
return CardStatistics(
total_amount=total_amount,
total_count=total_count,
top_amount_merchant=MerchantStat(
name=top_amount_merchant['name'],
amount=top_amount_merchant['amount'],
count=top_amount_merchant['count']
),
top_visit_merchant=MerchantStat(
name=top_visit_merchant['name'],
amount=top_visit_merchant['amount'],
count=top_visit_merchant['count']
),
category_stats=category_stats_list,
card_company_stats=card_company_stats_list,
monthly_stats=monthly_stats_list
)
Claude 3.7 Sonnet 이 만들어준 코드인데, 분석하는데도 한참 걸렸다.
아래에 있는 월별 사용금액 추이 차트는 canvas 를 사용했다.
<v-col cols="12">
<v-card elevation="2" class="rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon :color="getCardColorSync(selectedCardCompany)" class="mr-2">mdi-chart-line</v-icon>
월별 사용금액 추이
</v-card-title>
<v-card-text>
<div style="position: relative; height: 200px;">
<canvas ref="monthlyChart" height="200"></canvas>
</div>
</v-card-text>
</v-card>
</v-col>
선언부분은
const monthlyChart = ref<HTMLCanvasElement | null>(null);
차트는 Chart.js 를 사용했다(다른 부분에도 차트를 사용했는데, 거기서는 vue-chartjs 라는 모듈를 사용한다).
이 차트를 만들기 위해서 차트 데이터를 따로 가져오는 코드를 넣었다.
// 전체 데이터의 월별 통계 데이터 가져오기
const fetchAllMonthlyStats = async () => {
try {
// 월별 통계 전용 API 요청
const url = getApiUrl('/cards/all-monthly-stats');
console.log('전체 월별 통계 API 요청 URL:', url);
const response = await axios.get(url);
const monthlyStatsData = response.data;
// 월별 통계 데이터 처리
monthlyStats.value = monthlyStatsData;
// 월별 차트 초기화
await nextTick();
initMonthlyChart();
console.log('전체 월별 통계 데이터 받음:', monthlyStatsData);
} catch (error) {
console.error('월별 통계 데이터 조회 오류:', error);
// 오류 발생 시 빈 배열로 초기화
monthlyStats.value = [];
}
};
차트 데이터를 가져올 때 항상 초기화를 시켰는데..
초기화 시킬 때 nextTick 이라는 것을 사용했다.
아래는 Claude 3.7 Sonnet 이 알려주는 nextTick 에 대한 설명이다.
await nextTick() 부분은 Vue.js의 중요한 비동기 처리 메커니즘입니다.
이 코드에 대해 자세히 설명해 드리겠습니다.
nextTick 함수의 역할
nextTick은 Vue.js에서 제공하는 함수로, DOM 업데이트 사이클이 완료된 후에 특정 코드를 실행하도록 보장합니다.
Vue는 데이터 변경을 감지하면 DOM을 비동기적으로 업데이트하는데, nextTick은 이 DOM 업데이트가 완료된 "다음 틱(tick)"에 콜백을 실행합니다.
코드 맥락에서의 역할
// 월별 통계 데이터 처리
monthlyStats.value = monthlyStatsData;
// 월별 차트 초기화
await nextTick();
initMonthlyChart();
이 코드에서:
1. monthlyStats.value = monthlyStatsData;로 데이터를 업데이트합니다.
2. await nextTick();으로 DOM 업데이트가 완료될 때까지 기다립니다.
3. initMonthlyChart();로 차트를 초기화합니다.
왜 필요한가?
이 코드에서 nextTick이 필요한 이유는 다음과 같습니다:
1. DOM 의존성: initMonthlyChart 함수는 monthlyChart.value 참조를 통해 캔버스 DOM 요소에 접근합니다. 이 요소가 완전히 렌더링되어야 차트를 그릴 수 있습니다.
2. 데이터 반영 보장: monthlyStats.value가 업데이트된 후 Vue의 반응형 시스템이 이 변경을 처리하고 관련 DOM 업데이트를 완료할 시간이 필요합니다.
3. 차트 초기화 타이밍: 차트 라이브러리는 DOM 요소가 완전히 렌더링된 후에 제대로 작동합니다. nextTick을 사용하면 DOM이 업데이트된 후에 차트 초기화 코드가 실행되도록 보장할 수 있습니다.
실제 효과
이 코드가 없다면 다음과 같은 문제가 발생할 수 있습니다:
- 차트가 그려지지 않거나 부분적으로만 그려질 수 있습니다.
- 차트 크기가 잘못 계산될 수 있습니다.
- 데이터가 반영되지 않은 상태로 차트가 그려질 수 있습니다.
특히 월별 차트에서 데이터가 없는 달을 선택했다가 다시 데이터가 있는 달을 선택할 때 차트가 제대로 다시 그려지지 않는 문제를 해결하기 위해 이 코드가 중요합니다. nextTick을 사용함으로써 Vue의 반응형 시스템이 DOM을 완전히 업데이트한 후에 차트 초기화 코드가 실행되도록 보장합니다.