프로젝트 최초 커밋
This commit is contained in:
37
lib/utils/address_constants.dart
Normal file
37
lib/utils/address_constants.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
/// 주소 관련 상수 및 레이블 정의 파일
|
||||
///
|
||||
/// 한국의 시/도(광역시/도) 및 주소 입력 UI 레이블을 구분하여 관리합니다.
|
||||
|
||||
/// 한국의 시/도(광역시/도) 상수 클래스 (불변성 보장)
|
||||
class KoreanRegions {
|
||||
/// 최상위 행정구역(시/도)
|
||||
static const List<String> topLevel = [
|
||||
'서울특별시',
|
||||
'부산광역시',
|
||||
'대구광역시',
|
||||
'인천광역시',
|
||||
'광주광역시',
|
||||
'대전광역시',
|
||||
'울산광역시',
|
||||
'세종특별자치시',
|
||||
'경기도',
|
||||
'강원특별자치도',
|
||||
'충청북도',
|
||||
'충청남도',
|
||||
'전라북도',
|
||||
'전라남도',
|
||||
'경상북도',
|
||||
'경상남도',
|
||||
'제주특별자치도',
|
||||
];
|
||||
}
|
||||
|
||||
/// 주소 입력 관련 UI 레이블 상수 클래스
|
||||
class AddressLabels {
|
||||
static const String zipCode = '우편번호';
|
||||
static const String region = '시/도';
|
||||
static const String detail = '상세주소';
|
||||
static const String zipCodeHint = '우편번호를 입력하세요';
|
||||
static const String regionHint = '시/도를 선택하세요';
|
||||
static const String detailHint = '나머지 주소를 입력하세요';
|
||||
}
|
||||
59
lib/utils/constants.dart
Normal file
59
lib/utils/constants.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
/// 앱 전역에서 사용하는 상수 정의 파일
|
||||
///
|
||||
/// 라우트, 장비 상태, 장비 유형, 사용자 권한 등 도메인별로 구분하여 관리합니다.
|
||||
|
||||
/// 라우트 이름 상수 클래스
|
||||
class Routes {
|
||||
static const String home = '/';
|
||||
static const String equipment = '/equipment'; // 통합 장비 관리
|
||||
static const String equipmentIn = '/equipment-in'; // 입고 목록(미사용)
|
||||
static const String equipmentInAdd = '/equipment-in/add'; // 장비 입고 폼
|
||||
static const String equipmentInEdit = '/equipment-in/edit'; // 장비 입고 편집
|
||||
static const String equipmentOut = '/equipment-out'; // 출고 목록(미사용)
|
||||
static const String equipmentOutAdd = '/equipment-out/add'; // 장비 출고 폼
|
||||
static const String equipmentOutEdit = '/equipment-out/edit'; // 장비 출고 편집
|
||||
static const String equipmentInList = '/equipment/in'; // 입고 장비 목록
|
||||
static const String equipmentOutList = '/equipment/out'; // 출고 장비 목록
|
||||
static const String equipmentRentList = '/equipment/rent'; // 대여 장비 목록
|
||||
static const String company = '/company';
|
||||
static const String companyAdd = '/company/add';
|
||||
static const String companyEdit = '/company/edit';
|
||||
static const String user = '/user';
|
||||
static const String userAdd = '/user/add';
|
||||
static const String userEdit = '/user/edit';
|
||||
static const String license = '/license';
|
||||
static const String licenseAdd = '/license/add';
|
||||
static const String licenseEdit = '/license/edit';
|
||||
static const String warehouseLocation = '/warehouse-location'; // 입고지 관리 목록
|
||||
static const String warehouseLocationAdd =
|
||||
'/warehouse-location/add'; // 입고지 추가
|
||||
static const String warehouseLocationEdit =
|
||||
'/warehouse-location/edit'; // 입고지 수정
|
||||
static const String goods = '/goods'; // 물품 관리(등록)
|
||||
static const String goodsAdd = '/goods/add'; // 물품 등록 폼
|
||||
static const String goodsEdit = '/goods/edit'; // 물품 수정 폼
|
||||
}
|
||||
|
||||
/// 장비 상태 코드 상수 클래스
|
||||
class EquipmentStatus {
|
||||
static const String in_ = 'I'; // 입고
|
||||
static const String out = 'O'; // 출고
|
||||
static const String rent = 'T'; // 대여
|
||||
static const String repair = 'R'; // 수리
|
||||
static const String damaged = 'D'; // 손상
|
||||
static const String lost = 'L'; // 분실
|
||||
static const String etc = 'E'; // 기타
|
||||
}
|
||||
|
||||
/// 장비 유형 상수 클래스
|
||||
class EquipmentType {
|
||||
static const String new_ = '신제품'; // 신제품
|
||||
static const String used = '중고'; // 중고
|
||||
static const String contract = '계약'; // 계약(입고후 즉각 출고)
|
||||
}
|
||||
|
||||
/// 사용자 권한 상수 클래스
|
||||
class UserRoles {
|
||||
static const String admin = 'S'; // 관리자
|
||||
static const String member = 'M'; // 멤버
|
||||
}
|
||||
40
lib/utils/equipment_display_helper.dart
Normal file
40
lib/utils/equipment_display_helper.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
/// 장비 정보 표시를 위한 헬퍼 클래스 (SRP, 재사용성, 테스트 용이성 중심)
|
||||
class EquipmentDisplayHelper {
|
||||
/// 제조사명 포맷팅 (빈 값은 대시로 표시)
|
||||
static String formatManufacturer(String? manufacturer) {
|
||||
if (manufacturer == null || manufacturer.isEmpty) return '-';
|
||||
return manufacturer;
|
||||
}
|
||||
|
||||
/// 장비명 포맷팅 (빈 값은 대시로 표시)
|
||||
static String formatEquipmentName(String? name) {
|
||||
if (name == null || name.isEmpty) return '-';
|
||||
return name;
|
||||
}
|
||||
|
||||
/// 카테고리 포맷팅 (비어있지 않은 카테고리만 합침)
|
||||
static String formatCategory(
|
||||
String? category,
|
||||
String? subCategory,
|
||||
String? subSubCategory,
|
||||
) {
|
||||
final parts = [
|
||||
if (category != null && category.isNotEmpty) category,
|
||||
if (subCategory != null && subCategory.isNotEmpty) subCategory,
|
||||
if (subSubCategory != null && subSubCategory.isNotEmpty) subSubCategory,
|
||||
];
|
||||
if (parts.isEmpty) return '-';
|
||||
return parts.join(' > ');
|
||||
}
|
||||
|
||||
/// 시리얼 번호 포맷팅 (없으면 대시)
|
||||
static String formatSerialNumber(String? serialNumber) {
|
||||
return serialNumber?.isNotEmpty == true ? serialNumber! : '-';
|
||||
}
|
||||
|
||||
/// 날짜 포맷팅 (YYYY-MM-DD, null이면 대시)
|
||||
static String formatDate(DateTime? date) {
|
||||
if (date == null) return '-';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
127
lib/utils/phone_utils.dart
Normal file
127
lib/utils/phone_utils.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 전화번호 관련 유틸리티 클래스 (SRP, 재사용성, 테스트 용이성 중심)
|
||||
class PhoneUtils {
|
||||
/// 전화번호 입력 형식 지정용 InputFormatter
|
||||
static final TextInputFormatter phoneInputFormatter =
|
||||
_PhoneTextInputFormatter();
|
||||
|
||||
/// 전화번호 포맷팅 (뒤 4자리 하이픈)
|
||||
static String formatPhoneNumber(String phoneNumber) {
|
||||
final digitsOnly = phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digitsOnly.isEmpty) return '';
|
||||
if (digitsOnly.length > 8) {
|
||||
return formatPhoneNumber(digitsOnly.substring(0, 8));
|
||||
}
|
||||
if (digitsOnly.length > 4) {
|
||||
final frontPart = digitsOnly.substring(0, digitsOnly.length - 4);
|
||||
final backPart = digitsOnly.substring(digitsOnly.length - 4);
|
||||
return '$frontPart-$backPart';
|
||||
}
|
||||
return digitsOnly;
|
||||
}
|
||||
|
||||
/// 포맷된 전화번호에서 숫자만 추출
|
||||
static String extractDigitsOnly(String formattedPhoneNumber) {
|
||||
return formattedPhoneNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
}
|
||||
|
||||
/// 전체 전화번호에서 접두사 추출 (없으면 기본값)
|
||||
static String extractPhonePrefix(
|
||||
String fullNumber,
|
||||
List<String> phonePrefixes,
|
||||
) {
|
||||
if (fullNumber.isEmpty) return '010';
|
||||
String digitsOnly = fullNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
for (String prefix in phonePrefixes) {
|
||||
if (digitsOnly.startsWith(prefix)) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
return '010';
|
||||
}
|
||||
|
||||
/// 접두사 제외한 번호 추출
|
||||
static String extractPhoneNumberWithoutPrefix(
|
||||
String fullNumber,
|
||||
List<String> phonePrefixes,
|
||||
) {
|
||||
if (fullNumber.isEmpty) return '';
|
||||
String digitsOnly = fullNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
for (String prefix in phonePrefixes) {
|
||||
if (digitsOnly.startsWith(prefix)) {
|
||||
return digitsOnly.substring(prefix.length);
|
||||
}
|
||||
}
|
||||
return digitsOnly;
|
||||
}
|
||||
|
||||
/// 접두사와 번호를 합쳐 전체 전화번호 생성
|
||||
static String getFullPhoneNumber(String prefix, String number) {
|
||||
final remainingNumber = number.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (remainingNumber.isEmpty) return '';
|
||||
return '$prefix-$remainingNumber';
|
||||
}
|
||||
|
||||
/// 자주 사용되는 전화번호 접두사 목록 반환
|
||||
static List<String> getCommonPhonePrefixes() {
|
||||
return [
|
||||
'010',
|
||||
'011',
|
||||
'016',
|
||||
'017',
|
||||
'018',
|
||||
'019',
|
||||
'070',
|
||||
'080',
|
||||
'02',
|
||||
'031',
|
||||
'032',
|
||||
'033',
|
||||
'041',
|
||||
'042',
|
||||
'043',
|
||||
'044',
|
||||
'051',
|
||||
'052',
|
||||
'053',
|
||||
'054',
|
||||
'055',
|
||||
'061',
|
||||
'062',
|
||||
'063',
|
||||
'064',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// 전화번호 입력 형식 지정용 TextInputFormatter (내부 전용)
|
||||
class _PhoneTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
final digitsOnly = newValue.text.replaceAll(RegExp(r'[^\d]+'), '');
|
||||
final trimmed =
|
||||
digitsOnly.length > 11 ? digitsOnly.substring(0, 11) : digitsOnly;
|
||||
String formatted = '';
|
||||
if (trimmed.length > 7) {
|
||||
formatted =
|
||||
'${trimmed.substring(0, 3)}-${trimmed.substring(3, 7)}-${trimmed.substring(7)}';
|
||||
} else if (trimmed.length > 3) {
|
||||
formatted = '${trimmed.substring(0, 3)}-${trimmed.substring(3)}';
|
||||
} else {
|
||||
formatted = trimmed;
|
||||
}
|
||||
int selectionIndex =
|
||||
newValue.selection.end + (formatted.length - newValue.text.length);
|
||||
if (selectionIndex < 0) selectionIndex = 0;
|
||||
if (selectionIndex > formatted.length) selectionIndex = formatted.length;
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: selectionIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/utils/user_utils.dart
Normal file
16
lib/utils/user_utils.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
// 사용자 관련 유틸리티 함수 모음
|
||||
// 역할명 변환 등 공통 로직을 관리
|
||||
|
||||
import '../utils/constants.dart';
|
||||
|
||||
// 역할 코드 → 한글명 변환 함수
|
||||
String getRoleName(String role) {
|
||||
switch (role) {
|
||||
case UserRoles.admin:
|
||||
return '관리자';
|
||||
case UserRoles.member:
|
||||
return '일반 사용자';
|
||||
default:
|
||||
return '알 수 없음';
|
||||
}
|
||||
}
|
||||
136
lib/utils/validators.dart
Normal file
136
lib/utils/validators.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
/// 폼 필드 검증 함수 및 유틸리티 (SRP, 재사용성, 테스트 용이성 중심)
|
||||
|
||||
/// 필수 입력값 검증
|
||||
String? validateRequired(String? value, String fieldName) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$fieldName을(를) 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 이메일 형식 검증
|
||||
String? validateEmail(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;
|
||||
}
|
||||
|
||||
/// 전화번호 형식 검증 (숫자, 하이픈만 허용)
|
||||
String? validatePhoneNumber(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 필수 입력 아님
|
||||
}
|
||||
final phoneRegex = RegExp(r'^[0-9\-]+$');
|
||||
if (!phoneRegex.hasMatch(value)) {
|
||||
return '전화번호는 숫자와 하이픈(-)만 입력 가능합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 숫자 검증
|
||||
String? validateNumber(String? value, String fieldName) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$fieldName을(를) 입력해주세요';
|
||||
}
|
||||
final numberRegex = RegExp(r'^[0-9]+$');
|
||||
if (!numberRegex.hasMatch(value)) {
|
||||
return '$fieldName은(는) 숫자만 입력 가능합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 최소 길이 검증
|
||||
String? validateMinLength(String? value, String fieldName, int minLength) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$fieldName을(를) 입력해주세요';
|
||||
}
|
||||
if (value.length < minLength) {
|
||||
return '$fieldName은(는) 최소 $minLength자 이상이어야 합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 최대 길이 검증
|
||||
String? validateMaxLength(String? value, String fieldName, int maxLength) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 필수 입력 아님
|
||||
}
|
||||
if (value.length > maxLength) {
|
||||
return '$fieldName은(는) 최대 $maxLength자 이하여야 합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 시리얼 넘버 검증 (알파벳, 숫자, 하이픈만 허용)
|
||||
String? validateSerialNumber(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 필수 입력 아님
|
||||
}
|
||||
final serialRegex = RegExp(r'^[a-zA-Z0-9\-]+$');
|
||||
if (!serialRegex.hasMatch(value)) {
|
||||
return '시리얼 번호는 알파벳, 숫자, 하이픈(-)만 입력 가능합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 바코드 검증 (숫자만 허용)
|
||||
String? validateBarcode(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 필수 입력 아님
|
||||
}
|
||||
final barcodeRegex = RegExp(r'^[0-9]+$');
|
||||
if (!barcodeRegex.hasMatch(value)) {
|
||||
return '바코드는 숫자만 입력 가능합니다';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// FormValidator: 폼 필드 검증 유틸리티 클래스
|
||||
class FormValidator {
|
||||
/// 필수 입력 검증
|
||||
static String? Function(String?) required(String errorMessage) {
|
||||
return (String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return errorMessage;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/// 이메일 형식 검증
|
||||
static String? Function(String?) email([String? errorMessage]) {
|
||||
return (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 빈 값은 허용
|
||||
}
|
||||
final bool emailValid = RegExp(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||
).hasMatch(value);
|
||||
if (!emailValid) {
|
||||
return errorMessage ?? '유효한 이메일 주소를 입력하세요';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/// 전화번호 형식 검증
|
||||
static String? Function(String?) phone([String? errorMessage]) {
|
||||
return (String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null; // 빈 값은 허용
|
||||
}
|
||||
final bool phoneValid = RegExp(r'^[0-9\-\s]+$').hasMatch(value);
|
||||
if (!phoneValid) {
|
||||
return errorMessage ?? '유효한 전화번호를 입력하세요';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user