프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View 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
View 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'; // 멤버
}

View 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
View 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
View 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
View 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;
};
}
}