초기 커밋

This commit is contained in:
2026-03-01 07:55:59 +09:00
commit b0262d6bab
67 changed files with 4660 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import 'package:intl/intl.dart';
abstract final class AppDateUtils {
static final _dateFormat = DateFormat('yyyy-MM-dd');
static final _dateTimeFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
static final _timeFormat = DateFormat('HH:mm');
static final _koreanDateFormat = DateFormat('yyyy년 MM월 dd일');
static String formatDate(DateTime date) => _dateFormat.format(date);
static String formatDateTime(DateTime date) => _dateTimeFormat.format(date);
static String formatTime(DateTime date) => _timeFormat.format(date);
static String formatKoreanDate(DateTime date) => _koreanDateFormat.format(date);
static DateTime? tryParse(String? dateString) {
if (dateString == null) return null;
try {
return DateTime.parse(dateString);
} catch (_) {
return null;
}
}
static String timeAgo(DateTime date) {
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inDays > 365) {
return '${diff.inDays ~/ 365}년 전';
} else if (diff.inDays > 30) {
return '${diff.inDays ~/ 30}개월 전';
} else if (diff.inDays > 0) {
return '${diff.inDays}일 전';
} else if (diff.inHours > 0) {
return '${diff.inHours}시간 전';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}분 전';
} else {
return '방금 전';
}
}
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
extension BuildContextExtension on BuildContext {
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => Theme.of(this).textTheme;
ColorScheme get colorScheme => Theme.of(this).colorScheme;
MediaQueryData get mediaQuery => MediaQuery.of(this);
Size get screenSize => MediaQuery.sizeOf(this);
double get screenWidth => screenSize.width;
double get screenHeight => screenSize.height;
bool get isMobile => screenWidth < 600;
bool get isTablet => screenWidth >= 600 && screenWidth < 1200;
bool get isDesktop => screenWidth >= 1200;
void showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? colorScheme.error : null,
behavior: SnackBarBehavior.floating,
),
);
}
}
extension StringExtension on String {
String get capitalize =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
bool get isValidEmail => RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
).hasMatch(this);
bool get isValidPassword => length >= 8;
}
extension DateTimeExtension on DateTime {
String get toFormattedString => '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
String get toFormattedDateTime =>
'$toFormattedString ${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
}

View File

@@ -0,0 +1,45 @@
abstract final class Validators {
static String? email(String? value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(value)) {
return '올바른 이메일 형식이 아닙니다';
}
return null;
}
static String? password(String? value) {
if (value == null || value.isEmpty) {
return '비밀번호를 입력해주세요';
}
if (value.length < 8) {
return '비밀번호는 8자 이상이어야 합니다';
}
return null;
}
static String? required(String? value, [String? fieldName]) {
if (value == null || value.trim().isEmpty) {
return '${fieldName ?? '이 필드'}를 입력해주세요';
}
return null;
}
static String? minLength(String? value, int min, [String? fieldName]) {
if (value == null || value.length < min) {
return '${fieldName ?? '이 필드'}$min자 이상이어야 합니다';
}
return null;
}
static String? maxLength(String? value, int max, [String? fieldName]) {
if (value != null && value.length > max) {
return '${fieldName ?? '이 필드'}$max자 이하여야 합니다';
}
return null;
}
}