31) { return null; } $base = new DateTime($billingYearMonth . '-01'); if ($monthOffset !== 0) { $base->modify(($monthOffset > 0 ? '+' : '') . $monthOffset . ' month'); } $lastDay = (int)$base->format('t'); $realDay = min($day, $lastDay); $base->setDate( (int)$base->format('Y'), (int)$base->format('m'), $realDay ); return $base; } function account_has_statement_period(array $account): bool { return isset( $account['statement_start_month_offset'], $account['statement_start_day'], $account['statement_end_month_offset'], $account['statement_end_day'] ) && $account['statement_start_month_offset'] !== null && $account['statement_start_day'] !== null && $account['statement_end_month_offset'] !== null && $account['statement_end_day'] !== null && (int)$account['statement_start_day'] >= 1 && (int)$account['statement_start_day'] <= 31 && (int)$account['statement_end_day'] >= 1 && (int)$account['statement_end_day'] <= 31; } function transaction_date_in_card_statement_month(array $account, string $transactionDate, string $billingYearMonth): bool { $txTs = safe_date_ts($transactionDate); if ($txTs === null) { return false; } if (!account_has_statement_period($account)) { return false; } $start = card_month_day_date( $billingYearMonth, (int)$account['statement_start_month_offset'], (int)$account['statement_start_day'] ); $end = card_month_day_date( $billingYearMonth, (int)$account['statement_end_month_offset'], (int)$account['statement_end_day'] ); if (!$start || !$end) { return false; } $tx = new DateTime(date('Y-m-d', $txTs)); return $tx >= $start && $tx <= $end; } function get_card_billing_year_month_by_statement_period(array $account, string $transactionDate): ?string { $txTs = safe_date_ts($transactionDate); if ($txTs === null) { return null; } if (!account_has_statement_period($account)) { return null; } $tx = new DateTime(date('Y-m-d', $txTs)); /* * 청구월 기준 사용기간 예: * 2026-05 청구월 = 2026-04-11 ~ 2026-05-10 * * 거래일 주변의 청구월만 검사하면 됨. * 전월/당월/익월/익익월까지 여유 있게 검사. */ $base = new DateTime($tx->format('Y-m-01')); for ($i = -1; $i <= 2; $i++) { $candidate = clone $base; if ($i !== 0) { $candidate->modify(($i > 0 ? '+' : '') . $i . ' month'); } $candidateYm = $candidate->format('Y-m'); if (transaction_date_in_card_statement_month($account, $transactionDate, $candidateYm)) { return $candidateYm; } } return null; } function get_card_billing_year_month(array $account, string $transactionDate): ?string { if (($account['account_type'] ?? '') !== 'card') { return null; } $ts = safe_date_ts($transactionDate); if ($ts === null) { return null; } $cardKind = normalize_card_kind($account); // 체크카드는 즉시형으로 보고 거래월 그대로 if ($cardKind === 'check') { return date('Y-m', $ts); } // 신용카드인데 신용공여기간 계산을 안 쓰면 거래월 그대로 if (empty($account['use_credit_grace_period'])) { return date('Y-m', $ts); } // 신규 방식: 카드사별 실제 사용기간 설정 우선 $statementYm = get_card_billing_year_month_by_statement_period($account, $transactionDate); if ($statementYm !== null) { return $statementYm; } // fallback: 기존 billing_day 단순 방식 $billingDay = (int)($account['billing_day'] ?? 0); if ($billingDay <= 0 || $billingDay > 31) { return date('Y-m', $ts); } $dt = new DateTime(date('Y-m-d', $ts)); $day = (int)$dt->format('d'); if ($day <= $billingDay) { return $dt->format('Y-m'); } $dt->modify('first day of next month'); return $dt->format('Y-m'); } function get_card_payment_date(array $account, string $billingYearMonth): ?string { if (($account['account_type'] ?? '') !== 'card') { return null; } if (!preg_match('/^\d{4}-\d{2}$/', $billingYearMonth)) { return null; } $paymentDay = (int)($account['payment_day'] ?? 0); if ($paymentDay <= 0 || $paymentDay > 31) { return null; } [$year, $month] = explode('-', $billingYearMonth); $year = (int)$year; $month = (int)$month; $firstDay = new DateTime(sprintf('%04d-%02d-01', $year, $month)); $lastDay = (int)$firstDay->format('t'); $day = min($paymentDay, $lastDay); $firstDay->setDate($year, $month, $day); return $firstDay->format('Y-m-d'); } function get_card_statement_period_label(array $account): ?string { if (!account_has_statement_period($account)) { return null; } $startOffset = (int)$account['statement_start_month_offset']; $startDay = (int)$account['statement_start_day']; $endOffset = (int)$account['statement_end_month_offset']; $endDay = (int)$account['statement_end_day']; $monthText = function (int $offset): string { if ($offset === -2) return '전전월'; if ($offset === -1) return '전월'; if ($offset === 0) return '당월'; if ($offset === 1) return '익월'; if ($offset === 2) return '익익월'; return $offset . '개월'; }; return $monthText($startOffset) . ' ' . $startDay . '일 ~ ' . $monthText($endOffset) . ' ' . $endDay . '일 사용분'; } function get_card_billing_label(array $account): string { if (($account['account_type'] ?? '') !== 'card') { return '-'; } $cardKind = normalize_card_kind($account); if ($cardKind === 'check') { return '체크카드 · 즉시출금'; } if ($cardKind === 'credit') { $paymentDay = (int)($account['payment_day'] ?? 0); if (!empty($account['billing_cycle_memo'])) { return (string)$account['billing_cycle_memo']; } $periodLabel = get_card_statement_period_label($account); if ($periodLabel !== null && $paymentDay > 0) { return '신용카드 · ' . $periodLabel . ' / 납부일 ' . $paymentDay . '일'; } if ($periodLabel !== null) { return '신용카드 · ' . $periodLabel; } $billingDay = (int)($account['billing_day'] ?? 0); if ($billingDay > 0 && $paymentDay > 0) { return '신용카드 · 결제기준일 ' . $billingDay . '일 / 납부일 ' . $paymentDay . '일'; } return '신용카드'; } return '카드'; }