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을 완전히 업데이트한 후에 차트 초기화 코드가 실행되도록 보장합니다.