v7.0.0-beta.2 release
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData\Datasets;
|
||||
|
||||
/**
|
||||
* 인터넷 관련 데이터셋 (User Agent / Domain / TLD)
|
||||
*
|
||||
* Faker\Generator 대체용(FakerShim)에서 사용합니다.
|
||||
*/
|
||||
final class Internet
|
||||
{
|
||||
/** 샘플 User Agent 문자열 (데스크톱/모바일 혼합) */
|
||||
public const USER_AGENTS = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.91',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Linux; Android 14; SM-S918N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
||||
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-A536N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/105.0.0.0',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.0.0 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G998N Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/119.0.0.0 Mobile Safari/537.36',
|
||||
];
|
||||
|
||||
/** 샘플 도메인 */
|
||||
public const DOMAINS = [
|
||||
'example.com', 'example.net', 'example.org',
|
||||
'sample.co.kr', 'demo.co.kr', 'test.co.kr',
|
||||
'domain.io', 'service.co', 'platform.app',
|
||||
'shop.store', 'mall.kr', 'market.net',
|
||||
];
|
||||
|
||||
/** 이메일용 도메인 */
|
||||
public const EMAIL_DOMAINS = [
|
||||
'example.com', 'example.net', 'example.org',
|
||||
'sample.kr', 'demo.co.kr', 'mail.test',
|
||||
'post.kr', 'mailbox.co', 'inbox.me',
|
||||
];
|
||||
|
||||
/** TLD */
|
||||
public const TLDS = [
|
||||
'com', 'net', 'org', 'io', 'app', 'co', 'kr', 'co.kr', 'info', 'biz',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData\Datasets;
|
||||
|
||||
/**
|
||||
* 한국어 지리/주소 데이터셋
|
||||
*
|
||||
* Faker\Generator 대체용(FakerShim)에서 사용합니다.
|
||||
* 시×구×도로×번지 조합으로 주소 수만 개 생성 가능합니다.
|
||||
*/
|
||||
final class KoreanGeo
|
||||
{
|
||||
/** 광역시/도 (전체 17개) */
|
||||
public const STATES = [
|
||||
'서울특별시', '부산광역시', '인천광역시', '대구광역시', '대전광역시',
|
||||
'광주광역시', '울산광역시', '세종특별자치시',
|
||||
'경기도', '강원특별자치도', '충청북도', '충청남도',
|
||||
'전북특별자치도', '전라남도', '경상북도', '경상남도', '제주특별자치도',
|
||||
];
|
||||
|
||||
/** 시 (소규모 표시용) */
|
||||
public const CITIES = [
|
||||
'서울특별시', '부산광역시', '인천광역시', '대구광역시', '대전광역시',
|
||||
'광주광역시', '울산광역시', '수원시', '성남시', '용인시',
|
||||
'고양시', '부천시', '안양시', '안산시', '화성시',
|
||||
'평택시', '의정부시', '김해시', '청주시', '전주시',
|
||||
'포항시', '창원시', '춘천시', '원주시', '천안시',
|
||||
];
|
||||
|
||||
/** 구/군 (서울·부산·광역 혼합 흔한 이름) */
|
||||
public const DISTRICTS = [
|
||||
'강남구', '서초구', '송파구', '강동구', '종로구', '중구', '용산구', '성동구',
|
||||
'광진구', '동대문구', '중랑구', '성북구', '강북구', '도봉구', '노원구', '은평구',
|
||||
'서대문구', '마포구', '양천구', '강서구', '구로구', '금천구', '영등포구', '동작구',
|
||||
'관악구', '해운대구', '수영구', '남구', '북구', '분당구', '일산동구', '부평구',
|
||||
];
|
||||
|
||||
/** 도로명 */
|
||||
public const ROADS = [
|
||||
'테헤란로', '강남대로', '논현로', '봉은사로', '선릉로', '학동로', '삼성로', '영동대로',
|
||||
'남부순환로', '올림픽로', '양재대로', '세종대로', '이태원로', '한남대로', '연세로',
|
||||
'신촌로', '왕산로', '을지로', '퇴계로', '율곡로', '청계천로', '종로', '고덕로',
|
||||
'천호대로', '경인로', '공항대로', '김포한강로', '자유로', '강변북로', '동부간선로',
|
||||
];
|
||||
|
||||
/** 건물/상세주소 접미 */
|
||||
public const BUILDING_SUFFIX = [
|
||||
'아파트', '오피스텔', '빌라', '빌딩', '센터', '타워', '프라자', '하우스', '스테이션', '플레이스',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData\Datasets;
|
||||
|
||||
/**
|
||||
* 한국어 인명/기업명 데이터셋
|
||||
*
|
||||
* Faker\Generator 대체용(FakerShim)에서 사용합니다.
|
||||
* 성×이름 조합만 1,800개 이상, 회사명 조합은 3,000개 이상 생성 가능합니다.
|
||||
*/
|
||||
final class KoreanPeople
|
||||
{
|
||||
/** 한국 성씨 (통계청 상위 기반) */
|
||||
public const LAST_NAMES = [
|
||||
'김', '이', '박', '최', '정', '강', '조', '윤', '장', '임',
|
||||
'한', '오', '서', '신', '권', '황', '안', '송', '전', '홍',
|
||||
'유', '고', '문', '양', '손', '배', '백', '허', '남', '심',
|
||||
'노', '하', '곽', '성', '차', '주', '우', '구', '민', '류',
|
||||
];
|
||||
|
||||
/** 한국어 이름 (남녀 혼합, 흔히 쓰이는 현대 이름 중심) */
|
||||
public const FIRST_NAMES = [
|
||||
'민준', '서준', '예준', '도윤', '시우', '주원', '하준', '지호', '지훈', '건우',
|
||||
'우진', '선우', '서진', '현우', '민재', '현준', '연우', '유준', '정우', '승우',
|
||||
'승현', '지원', '재윤', '도현', '지안', '유찬', '이준', '시윤', '은우', '예성',
|
||||
'서연', '서윤', '지우', '서현', '하은', '민서', '유진', '윤서', '지민', '채원',
|
||||
'수아', '지유', '예린', '수빈', '소율', '시은', '가은', '예진', '소연', '은서',
|
||||
'아린', '소윤', '수연', '예원', '혜원', '다은', '나연', '유나', '지아', '서영',
|
||||
];
|
||||
|
||||
/** 회사명 접두 (지역/업종 수식 포함) */
|
||||
public const COMPANY_PREFIX = [
|
||||
'가나', '대한', '한국', '서울', '대명', '동양', '신한', '동방', '한미', '삼미',
|
||||
'금강', '은성', '천일', '현대', '미래', '새한', '우리', '광명', '청솔', '태평',
|
||||
'아이', '스마트', '유니', '디지털', '퓨처', '코어', '비전', '크리에이티브', '라이트', '이노',
|
||||
'네오', '에코', '글로벌', '퍼스트', '그린', '블루', '프라임', '메가', '오션', '실버',
|
||||
];
|
||||
|
||||
/** 회사명 접미 (업종/규모 지정) */
|
||||
public const COMPANY_SUFFIX = [
|
||||
'테크', '소프트', '시스템즈', '솔루션', '코리아', '인더스트리', '엔터프라이즈', '이노베이션', '랩스', '그룹',
|
||||
'컴퍼니', '네트웍스', '미디어', '커뮤니케이션즈', '파트너스', '홀딩스', '서비스', '퍼니처', '로지스틱스', '인터내셔널',
|
||||
'주식회사', '(주)', '산업', '전자', '제약', '상사', '물산', '식품', '화학', '건설',
|
||||
];
|
||||
|
||||
/** 직책/직급 */
|
||||
public const JOB_TITLES = [
|
||||
'사원', '주임', '대리', '과장', '차장', '부장', '팀장', '실장', '이사', '상무',
|
||||
'전무', '부사장', '사장', '회장', '연구원', '선임연구원', '책임연구원', '개발자', '디자이너', '기획자',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData\Datasets;
|
||||
|
||||
/**
|
||||
* 한국어 단어/문장 재료 데이터셋
|
||||
*
|
||||
* Faker\Generator 대체용(FakerShim)의 word/sentence/paragraph 에 사용합니다.
|
||||
* 일상·업무·기술 혼합 명사/형용사/동사 약 220개로 구성해
|
||||
* 다양한 문장 조합을 생성할 수 있습니다.
|
||||
*/
|
||||
final class KoreanText
|
||||
{
|
||||
/** 한국어 단어 풀 (명사/형용사/동사 혼합) */
|
||||
public const WORDS = [
|
||||
// 일상 명사
|
||||
'시간', '사람', '사회', '나라', '문제', '생각', '정도', '정보', '이야기', '오늘',
|
||||
'필요', '세상', '회사', '시장', '관계', '경우', '경제', '결과', '가능성', '교육',
|
||||
'문화', '지역', '정책', '활동', '모습', '사용', '생활', '의미', '대학', '기업',
|
||||
'개발', '서비스', '기술', '환경', '지원', '과정', '선택', '상황', '영향', '중요성',
|
||||
// 업무/기술
|
||||
'프로젝트', '시스템', '플랫폼', '솔루션', '프로세스', '워크플로우', '데이터', '알고리즘', '구조', '설계',
|
||||
'분석', '전략', '운영', '관리', '성과', '품질', '생산성', '효율성', '안정성', '신뢰성',
|
||||
'보안', '확장성', '유연성', '통합', '최적화', '자동화', '협업', '소통', '혁신', '지속가능성',
|
||||
// 형용사
|
||||
'새로운', '다양한', '안정적인', '효율적인', '혁신적인', '편리한', '빠른', '간결한', '체계적인', '직관적인',
|
||||
'정확한', '세심한', '꼼꼼한', '유연한', '강력한', '특별한', '독특한', '전문적인', '일관된', '창의적인',
|
||||
// 동사 어간 (문장에서 ‘-합니다’ 접미로 사용)
|
||||
'제공', '지원', '개선', '강화', '확대', '구축', '설정', '구성', '배포', '유지',
|
||||
'개발', '운영', '점검', '검토', '측정', '추적', '공유', '전달', '수행', '완료',
|
||||
'준비', '제안', '반영', '해결', '예방', '탐지', '분석', '설계', '실행', '검증',
|
||||
// 감각/태도
|
||||
'감사', '성공', '실패', '기회', '가치', '능력', '책임', '노력', '신뢰', '열정',
|
||||
'성장', '도전', '배움', '발견', '깨달음', '용기', '인내', '집중', '몰입', '균형',
|
||||
// 제품/소비 맥락
|
||||
'상품', '제품', '주문', '배송', '결제', '할인', '쿠폰', '포인트', '리뷰', '문의',
|
||||
'혜택', '행사', '이벤트', '프로모션', '구독', '멤버십', '등급', '랭킹', '후기', '장바구니',
|
||||
'카테고리', '브랜드', '신상품', '베스트셀러', '한정판', '재고', '사이즈', '컬러', '디자인', '소재',
|
||||
];
|
||||
|
||||
/** 문장 앞에 붙는 접속부/부사 (자연스러운 운율용) */
|
||||
public const SENTENCE_HEADS = [
|
||||
'오늘', '어제', '최근', '현재', '앞으로', '지금', '바로', '특히', '무엇보다', '그리고',
|
||||
'또한', '한편', '다만', '결국', '따라서', '이제', '여전히', '아직', '곧', '이미',
|
||||
];
|
||||
|
||||
/** 이메일 사용자 접미 */
|
||||
public const EMAIL_USER_SUFFIX = [
|
||||
'kim', 'lee', 'park', 'choi', 'jung', 'han', 'seo', 'min', 'ji', 'hyun',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData;
|
||||
|
||||
/**
|
||||
* FakerFactoryShim — Faker\Factory 대체 구현
|
||||
*
|
||||
* fakerphp/faker 패키지가 설치되지 않은 환경에서 \Faker\Factory::create($locale) 호출을 대체합니다.
|
||||
* Laravel 의 fake() 헬퍼(framework helpers.php)가 \Faker\Factory::create() 를 내부 호출하므로,
|
||||
* 이 Shim 을 class_alias(FakerFactoryShim::class, \Faker\Factory::class) 로 등록해두면
|
||||
* Laravel fake() 도 자동으로 FakerShim 을 반환하게 됩니다.
|
||||
*
|
||||
* 등록 위치: app/Support/SampleData/bootstrap.php (composer autoload.files 진입점)
|
||||
*/
|
||||
class FakerFactoryShim
|
||||
{
|
||||
/**
|
||||
* Faker\Factory::create() 호환 시그니처
|
||||
*
|
||||
* @return FakerShim FakerShim 인스턴스 (class_alias 로 \Faker\Generator 타입과 호환)
|
||||
*/
|
||||
public static function create(string $locale = 'ko_KR'): FakerShim
|
||||
{
|
||||
return new FakerShim($locale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData;
|
||||
|
||||
use App\Support\SampleData\Datasets\Internet;
|
||||
use App\Support\SampleData\Datasets\KoreanGeo;
|
||||
use App\Support\SampleData\Datasets\KoreanPeople;
|
||||
use App\Support\SampleData\Datasets\KoreanText;
|
||||
use BadMethodCallException;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* FakerShim — Faker\Generator 대체 구현
|
||||
*
|
||||
* fakerphp/faker 패키지가 설치되지 않은 환경(예: composer --no-dev)에서도
|
||||
* 샘플 시더와 팩토리가 동작하도록 Faker\Generator 와 동일한 공용 API 를 제공합니다.
|
||||
*
|
||||
* 등록 방법: AppServiceProvider::register() 에서 Faker 미설치 감지 시
|
||||
* class_alias(self::class, \Faker\Generator::class)
|
||||
* 로 바인딩하면 Laravel Factory 와 fake() 전역 함수가 이 클래스를 사용합니다.
|
||||
*
|
||||
* 데이터 품질 전략:
|
||||
* - 순수 랜덤 메서드(randomElement, numberBetween 등) — Faker 와 결과 구별 불가
|
||||
* - 한국어 데이터(name, address, word 등) — Datasets/*.php 풀에서 조합 생성
|
||||
* - 미구현 메서드는 BadMethodCallException — 필요 시 본 클래스에 추가 구현
|
||||
*/
|
||||
class FakerShim
|
||||
{
|
||||
public string $locale;
|
||||
|
||||
/**
|
||||
* unique() 추적 캐시 [methodKey => [hash => true]]
|
||||
*
|
||||
* @var array<string, array<string, true>>
|
||||
*/
|
||||
protected array $seenUniques = [];
|
||||
|
||||
public function __construct(string $locale = 'ko_KR')
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 순수 랜덤
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @param array<T> $array
|
||||
* @return T|null
|
||||
*/
|
||||
public function randomElement(array $array = ['a', 'b', 'c'])
|
||||
{
|
||||
if (empty($array)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $array[array_rand($array)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @param array<T> $array
|
||||
* @return array<T>
|
||||
*/
|
||||
public function randomElements(array $array = ['a', 'b', 'c'], int $count = 1, bool $allowDuplicates = false): array
|
||||
{
|
||||
if (empty($array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
if ($allowDuplicates) {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$result[] = $array[array_rand($array)];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$count = min($count, count($array));
|
||||
$keys = (array) array_rand($array, $count);
|
||||
foreach ($keys as $key) {
|
||||
$result[] = $array[$key];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function randomDigit(): int
|
||||
{
|
||||
return random_int(0, 9);
|
||||
}
|
||||
|
||||
public function randomDigitNotNull(): int
|
||||
{
|
||||
return random_int(1, 9);
|
||||
}
|
||||
|
||||
public function randomNumber(?int $nbDigits = null, bool $strict = false): int
|
||||
{
|
||||
$nbDigits = $nbDigits ?? random_int(1, 9);
|
||||
$max = (int) (10 ** $nbDigits) - 1;
|
||||
$min = $strict && $nbDigits > 1 ? (int) (10 ** ($nbDigits - 1)) : 0;
|
||||
|
||||
return random_int($min, $max);
|
||||
}
|
||||
|
||||
public function numberBetween(int $min = 0, int $max = 2147483647): int
|
||||
{
|
||||
if ($min > $max) {
|
||||
[$min, $max] = [$max, $min];
|
||||
}
|
||||
|
||||
return random_int($min, $max);
|
||||
}
|
||||
|
||||
public function randomFloat(?int $decimals = null, int|float $min = 0, int|float|null $max = null): float
|
||||
{
|
||||
if ($max === null) {
|
||||
$max = $min + 100;
|
||||
}
|
||||
if ($decimals === null) {
|
||||
$decimals = random_int(0, 4);
|
||||
}
|
||||
$value = $min + (mt_rand() / mt_getrandmax()) * ($max - $min);
|
||||
|
||||
return round($value, $decimals);
|
||||
}
|
||||
|
||||
public function boolean(int $chanceOfGettingTrue = 50): bool
|
||||
{
|
||||
return random_int(1, 100) <= $chanceOfGettingTrue;
|
||||
}
|
||||
|
||||
public function numerify(string $string = '###'): string
|
||||
{
|
||||
return preg_replace_callback('/#/', fn () => (string) random_int(0, 9), $string);
|
||||
}
|
||||
|
||||
public function lexify(string $string = '????'): string
|
||||
{
|
||||
return preg_replace_callback('/\?/', fn () => chr(random_int(97, 122)), $string);
|
||||
}
|
||||
|
||||
public function bothify(string $string = '## ??'): string
|
||||
{
|
||||
return $this->lexify($this->numerify($string));
|
||||
}
|
||||
|
||||
public function asciify(string $string = '****'): string
|
||||
{
|
||||
return preg_replace_callback('/\*/', fn () => chr(random_int(33, 126)), $string);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 인터넷
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function ipv4(): string
|
||||
{
|
||||
return random_int(1, 254).'.'.random_int(0, 255).'.'.random_int(0, 255).'.'.random_int(1, 254);
|
||||
}
|
||||
|
||||
public function ipv6(): string
|
||||
{
|
||||
$parts = [];
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$parts[] = dechex(random_int(0, 65535));
|
||||
}
|
||||
|
||||
return implode(':', $parts);
|
||||
}
|
||||
|
||||
public function macAddress(): string
|
||||
{
|
||||
$parts = [];
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$parts[] = str_pad(dechex(random_int(0, 255)), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return implode(':', $parts);
|
||||
}
|
||||
|
||||
public function userAgent(): string
|
||||
{
|
||||
return $this->randomElement(Internet::USER_AGENTS);
|
||||
}
|
||||
|
||||
public function url(): string
|
||||
{
|
||||
return 'https://'.$this->randomElement(Internet::DOMAINS).'/'.$this->slug(3, false);
|
||||
}
|
||||
|
||||
public function domainName(): string
|
||||
{
|
||||
return $this->randomElement(Internet::DOMAINS);
|
||||
}
|
||||
|
||||
public function tld(): string
|
||||
{
|
||||
return $this->randomElement(Internet::TLDS);
|
||||
}
|
||||
|
||||
public function email(): string
|
||||
{
|
||||
$user = $this->randomElement(KoreanText::EMAIL_USER_SUFFIX).$this->randomNumber(3, true);
|
||||
|
||||
return $user.'@'.$this->randomElement(Internet::EMAIL_DOMAINS);
|
||||
}
|
||||
|
||||
public function safeEmail(): string
|
||||
{
|
||||
return $this->email();
|
||||
}
|
||||
|
||||
public function userName(): string
|
||||
{
|
||||
return $this->randomElement(KoreanText::EMAIL_USER_SUFFIX).$this->randomNumber(3, true);
|
||||
}
|
||||
|
||||
public function password(int $minLength = 6, int $maxLength = 20): string
|
||||
{
|
||||
$length = random_int($minLength, $maxLength);
|
||||
|
||||
return $this->bothify(str_repeat('?', (int) floor($length / 2)).str_repeat('#', (int) ceil($length / 2)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 식별자
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function uuid(): string
|
||||
{
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
public function semver(bool $preRelease = false, bool $build = false): string
|
||||
{
|
||||
$major = random_int(0, 9);
|
||||
$minor = random_int(0, 20);
|
||||
$patch = random_int(0, 50);
|
||||
$version = "{$major}.{$minor}.{$patch}";
|
||||
|
||||
if ($preRelease) {
|
||||
$version .= '-'.$this->randomElement(['alpha', 'beta', 'rc']).'.'.random_int(1, 9);
|
||||
}
|
||||
if ($build) {
|
||||
$version .= '+'.$this->lexify('???').random_int(100, 999);
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
public function slug(int $nbWords = 6, bool $variableNbWords = true): string
|
||||
{
|
||||
if ($variableNbWords) {
|
||||
$nbWords = max(1, $nbWords + random_int(-2, 2));
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
for ($i = 0; $i < $nbWords; $i++) {
|
||||
$parts[] = $this->lexify(str_repeat('?', random_int(4, 8)));
|
||||
}
|
||||
|
||||
return implode('-', $parts);
|
||||
}
|
||||
|
||||
public function md5(): string
|
||||
{
|
||||
return md5((string) random_int(0, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
public function sha1(): string
|
||||
{
|
||||
return sha1((string) random_int(0, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
public function sha256(): string
|
||||
{
|
||||
return hash('sha256', (string) random_int(0, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 날짜/시간
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function dateTime(string|int $max = 'now', ?string $timezone = null): DateTime
|
||||
{
|
||||
$maxTs = is_int($max) ? $max : (int) strtotime((string) $max);
|
||||
$ts = random_int(0, max(0, $maxTs));
|
||||
$dt = new DateTime('@'.$ts);
|
||||
$dt->setTimezone(new DateTimeZone($timezone ?? date_default_timezone_get()));
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
public function dateTimeBetween(string|int $startDate = '-30 years', string|int $endDate = 'now', ?string $timezone = null): DateTime
|
||||
{
|
||||
$start = is_int($startDate) ? $startDate : (int) strtotime((string) $startDate);
|
||||
$end = is_int($endDate) ? $endDate : (int) strtotime((string) $endDate);
|
||||
if ($start > $end) {
|
||||
[$start, $end] = [$end, $start];
|
||||
}
|
||||
$ts = random_int($start, $end);
|
||||
$dt = new DateTime('@'.$ts);
|
||||
$dt->setTimezone(new DateTimeZone($timezone ?? date_default_timezone_get()));
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
public function dateTimeThisYear(string|int $max = 'now'): DateTime
|
||||
{
|
||||
return $this->dateTimeBetween(strtotime('first day of January this year'), $max);
|
||||
}
|
||||
|
||||
public function dateTimeThisMonth(string|int $max = 'now'): DateTime
|
||||
{
|
||||
return $this->dateTimeBetween(strtotime('first day of this month'), $max);
|
||||
}
|
||||
|
||||
public function date(string $format = 'Y-m-d', string|int $max = 'now'): string
|
||||
{
|
||||
return $this->dateTime($max)->format($format);
|
||||
}
|
||||
|
||||
public function time(string $format = 'H:i:s', string|int $max = 'now'): string
|
||||
{
|
||||
return $this->dateTime($max)->format($format);
|
||||
}
|
||||
|
||||
public function unixTime(string|int $max = 'now'): int
|
||||
{
|
||||
$maxTs = is_int($max) ? $max : (int) strtotime((string) $max);
|
||||
|
||||
return random_int(0, max(0, $maxTs));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 한국어 인명/회사
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function name(?string $gender = null): string
|
||||
{
|
||||
return $this->lastName().$this->firstName();
|
||||
}
|
||||
|
||||
public function firstName(?string $gender = null): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::FIRST_NAMES);
|
||||
}
|
||||
|
||||
public function lastName(): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::LAST_NAMES);
|
||||
}
|
||||
|
||||
public function title(?string $gender = null): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::JOB_TITLES);
|
||||
}
|
||||
|
||||
public function company(): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::COMPANY_PREFIX).$this->randomElement(KoreanPeople::COMPANY_SUFFIX);
|
||||
}
|
||||
|
||||
public function companySuffix(): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::COMPANY_SUFFIX);
|
||||
}
|
||||
|
||||
public function jobTitle(): string
|
||||
{
|
||||
return $this->randomElement(KoreanPeople::JOB_TITLES);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 주소
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function address(): string
|
||||
{
|
||||
$city = $this->randomElement(KoreanGeo::CITIES);
|
||||
$district = $this->randomElement(KoreanGeo::DISTRICTS);
|
||||
$road = $this->randomElement(KoreanGeo::ROADS);
|
||||
|
||||
return "{$city} {$district} {$road} ".random_int(1, 999);
|
||||
}
|
||||
|
||||
public function streetAddress(): string
|
||||
{
|
||||
return $this->randomElement(KoreanGeo::ROADS).' '.random_int(1, 999);
|
||||
}
|
||||
|
||||
public function secondaryAddress(): string
|
||||
{
|
||||
return random_int(1, 50).'층 '.random_int(100, 9999).'호';
|
||||
}
|
||||
|
||||
public function city(): string
|
||||
{
|
||||
return $this->randomElement(KoreanGeo::CITIES);
|
||||
}
|
||||
|
||||
public function state(): string
|
||||
{
|
||||
return $this->randomElement(KoreanGeo::STATES);
|
||||
}
|
||||
|
||||
public function stateAbbr(): string
|
||||
{
|
||||
return substr((string) $this->state(), 0, 2);
|
||||
}
|
||||
|
||||
public function country(): string
|
||||
{
|
||||
return '대한민국';
|
||||
}
|
||||
|
||||
public function countryCode(): string
|
||||
{
|
||||
return $this->randomElement(['KR', 'US', 'JP', 'CN', 'GB', 'DE', 'FR', 'IT', 'SG', 'TW']);
|
||||
}
|
||||
|
||||
public function postcode(): string
|
||||
{
|
||||
return $this->numerify('#####');
|
||||
}
|
||||
|
||||
public function postalCode(): string
|
||||
{
|
||||
return $this->postcode();
|
||||
}
|
||||
|
||||
public function phoneNumber(): string
|
||||
{
|
||||
return '010-'.$this->numerify('####-####');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 텍스트
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function word(): string
|
||||
{
|
||||
return $this->randomElement(KoreanText::WORDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>|string
|
||||
*/
|
||||
public function words(int $nb = 3, bool $asText = false)
|
||||
{
|
||||
$words = [];
|
||||
for ($i = 0; $i < $nb; $i++) {
|
||||
$words[] = $this->word();
|
||||
}
|
||||
|
||||
return $asText ? implode(' ', $words) : $words;
|
||||
}
|
||||
|
||||
public function sentence(int $nbWords = 6, bool $variableNbWords = true): string
|
||||
{
|
||||
if ($variableNbWords) {
|
||||
$nbWords = max(1, $nbWords + random_int(-2, 2));
|
||||
}
|
||||
|
||||
$head = $this->randomElement(KoreanText::SENTENCE_HEADS);
|
||||
$words = $this->words($nbWords, false);
|
||||
|
||||
return $head.' '.implode(' ', $words).'.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>|string
|
||||
*/
|
||||
public function sentences(int $nb = 3, bool $asText = false)
|
||||
{
|
||||
$sentences = [];
|
||||
for ($i = 0; $i < $nb; $i++) {
|
||||
$sentences[] = $this->sentence();
|
||||
}
|
||||
|
||||
return $asText ? implode(' ', $sentences) : $sentences;
|
||||
}
|
||||
|
||||
public function paragraph(int $nbSentences = 3, bool $variableNbSentences = true): string
|
||||
{
|
||||
if ($variableNbSentences) {
|
||||
$nbSentences = max(1, $nbSentences + random_int(-1, 1));
|
||||
}
|
||||
|
||||
return $this->sentences($nbSentences, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>|string
|
||||
*/
|
||||
public function paragraphs(int $nb = 3, bool $asText = false)
|
||||
{
|
||||
$paragraphs = [];
|
||||
for ($i = 0; $i < $nb; $i++) {
|
||||
$paragraphs[] = $this->paragraph();
|
||||
}
|
||||
|
||||
return $asText ? implode("\n\n", $paragraphs) : $paragraphs;
|
||||
}
|
||||
|
||||
public function text(int $maxNbChars = 200): string
|
||||
{
|
||||
$text = '';
|
||||
while (mb_strlen($text) < $maxNbChars) {
|
||||
$text .= $this->sentence().' ';
|
||||
}
|
||||
|
||||
return rtrim(mb_substr($text, 0, $maxNbChars));
|
||||
}
|
||||
|
||||
public function realText(int $maxNbChars = 200): string
|
||||
{
|
||||
return $this->text($maxNbChars);
|
||||
}
|
||||
|
||||
public function realTextBetween(int $minNbChars = 160, int $maxNbChars = 200): string
|
||||
{
|
||||
$target = random_int($minNbChars, $maxNbChars);
|
||||
|
||||
return $this->text($target);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 기타 편의
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function hexColor(): string
|
||||
{
|
||||
return '#'.str_pad(dechex(random_int(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function safeHexColor(): string
|
||||
{
|
||||
return $this->hexColor();
|
||||
}
|
||||
|
||||
public function rgbColor(): string
|
||||
{
|
||||
return random_int(0, 255).','.random_int(0, 255).','.random_int(0, 255);
|
||||
}
|
||||
|
||||
public function colorName(): string
|
||||
{
|
||||
return $this->randomElement(['red', 'blue', 'green', 'yellow', 'orange', 'purple', 'black', 'white', 'gray', 'pink']);
|
||||
}
|
||||
|
||||
public function imageUrl(int $width = 640, int $height = 480, ?string $category = null): string
|
||||
{
|
||||
return "https://via.placeholder.com/{$width}x{$height}";
|
||||
}
|
||||
|
||||
public function image(...$args): string
|
||||
{
|
||||
$width = $args[1] ?? 640;
|
||||
$height = $args[2] ?? 480;
|
||||
|
||||
return $this->imageUrl($width, $height);
|
||||
}
|
||||
|
||||
public function mimeType(): string
|
||||
{
|
||||
return $this->randomElement(['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain', 'application/json']);
|
||||
}
|
||||
|
||||
public function fileExtension(): string
|
||||
{
|
||||
return $this->randomElement(['jpg', 'png', 'gif', 'pdf', 'txt', 'json', 'csv', 'xlsx', 'docx']);
|
||||
}
|
||||
|
||||
public function currencyCode(): string
|
||||
{
|
||||
return $this->randomElement(['KRW', 'USD', 'JPY', 'EUR', 'CNY', 'GBP']);
|
||||
}
|
||||
|
||||
public function regexify(string $regex = ''): string
|
||||
{
|
||||
return $regex !== '' ? $regex : $this->lexify('????');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 수식자 (modifier)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function unique(bool $reset = false, int $maxRetries = 10000): UniqueShim
|
||||
{
|
||||
if ($reset) {
|
||||
$this->seenUniques = [];
|
||||
}
|
||||
|
||||
return new UniqueShim($this, $this->seenUniques, $maxRetries);
|
||||
}
|
||||
|
||||
public function optional(float|int $weight = 0.5, mixed $default = null): OptionalShim
|
||||
{
|
||||
return new OptionalShim($this, $weight, $default);
|
||||
}
|
||||
|
||||
public function valid(?callable $validator = null, int $maxRetries = 10000): self
|
||||
{
|
||||
// Simple passthrough. Faker 의 valid() 는 콜백으로 필터링하지만
|
||||
// 샘플 데이터 범위에서는 사실상 패스스루면 충분.
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 호환성 진입점 (속성 접근)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** Faker\Generator 는 locale 을 public property 로 노출함 */
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
if ($name === 'locale') {
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 미구현 메서드 호출 시 명시적 예외 */
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
throw new BadMethodCallException(
|
||||
"FakerShim: 메서드 '{$name}'() 는 구현되지 않았습니다. ".
|
||||
'App\\Support\\SampleData\\FakerShim 에 추가 구현하거나 fakerphp/faker 를 설치하세요.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData;
|
||||
|
||||
/**
|
||||
* FakerShim::optional() 프록시
|
||||
*
|
||||
* Faker 와 동일한 API 제공:
|
||||
* $faker->optional()->sentence() // 50% 확률로 null
|
||||
* $faker->optional(0.7)->name() // 70% 확률로 name, 30% null
|
||||
* $faker->optional(0.3, 'default')->x() // 30% 확률 x, 70% default
|
||||
*
|
||||
* weight 해석:
|
||||
* - 0.0 ~ 1.0 : 확률
|
||||
* - 1 초과 값 : Faker 관례상 정수 0-100 퍼센트로 간주
|
||||
*/
|
||||
class OptionalShim
|
||||
{
|
||||
public function __construct(
|
||||
private FakerShim $shim,
|
||||
private float|int $weight,
|
||||
private mixed $default = null,
|
||||
) {}
|
||||
|
||||
public function __call(string $method, array $arguments): mixed
|
||||
{
|
||||
$threshold = $this->weight <= 1
|
||||
? (int) round($this->weight * 100)
|
||||
: (int) $this->weight;
|
||||
|
||||
if (random_int(1, 100) <= $threshold) {
|
||||
return $this->shim->{$method}(...$arguments);
|
||||
}
|
||||
|
||||
return $this->default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\SampleData;
|
||||
|
||||
use OverflowException;
|
||||
|
||||
/**
|
||||
* FakerShim::unique() 프록시
|
||||
*
|
||||
* Faker\UniqueGenerator 와 동일한 API 제공:
|
||||
* $faker->unique()->name() → 이전에 반환한 값과 겹치지 않을 때까지 재시도
|
||||
*
|
||||
* 사용한 (메서드 × 인자) 조합마다 독립된 중복 방지 집합을 유지합니다.
|
||||
*/
|
||||
class UniqueShim
|
||||
{
|
||||
/**
|
||||
* @param array<string, array<string, true>> $seen
|
||||
*/
|
||||
public function __construct(
|
||||
private FakerShim $shim,
|
||||
private array &$seen,
|
||||
private int $maxRetries = 10000,
|
||||
) {}
|
||||
|
||||
public function __call(string $method, array $arguments): mixed
|
||||
{
|
||||
$bucket = $method.':'.md5(serialize($arguments));
|
||||
if (! isset($this->seen[$bucket])) {
|
||||
$this->seen[$bucket] = [];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->maxRetries; $i++) {
|
||||
$value = $this->shim->{$method}(...$arguments);
|
||||
$hash = is_scalar($value) ? (string) $value : md5(serialize($value));
|
||||
if (! isset($this->seen[$bucket][$hash])) {
|
||||
$this->seen[$bucket][$hash] = true;
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new OverflowException(
|
||||
"UniqueShim: '{$method}' 에서 {$this->maxRetries} 회 재시도 후에도 고유 값을 생성하지 못했습니다. ".
|
||||
'데이터셋 풀이 너무 작거나 요청 수가 데이터셋보다 많은지 확인하세요.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* FakerShim Bootstrap
|
||||
*
|
||||
* composer autoload.files 진입점으로, vendor/autoload.php 로드 직후 실행됩니다.
|
||||
* laravel/framework 의 helpers.php (autoload.files) 보다 나중에 실행되므로,
|
||||
* Faker 미설치 환경에서 Laravel 이 놓친 fake() 정의를 대체하고
|
||||
* \Faker\Factory, \Faker\Generator 를 Shim 으로 alias 합니다.
|
||||
*
|
||||
* 감지 전략:
|
||||
* - class_exists(..., false) 는 lazy-load 된 상태를 구분하지 못하므로 사용하지 않음
|
||||
* - file_exists 로 vendor/fakerphp/faker 설치 여부 직접 확인 (OS 파일시스템 캐시 활용, 수십 ns)
|
||||
*
|
||||
* 등록 시점:
|
||||
* - 매 요청당 1회 (PHP-FPM 워커 수명 동안 캐시되지는 않음 — autoload.files 는 매 요청 require)
|
||||
* - 비용: file_exists ~100ns + class_alias ~500ns (Faker 부재 시) = 총 1μs 미만
|
||||
*/
|
||||
|
||||
$fakerInstalled = file_exists(__DIR__.'/../../../vendor/fakerphp/faker/src/Faker/Factory.php');
|
||||
|
||||
if (! $fakerInstalled) {
|
||||
// Faker 미설치: Shim 을 실제 Faker 클래스명으로 alias
|
||||
if (! class_exists(\Faker\Factory::class, false)) {
|
||||
class_alias(\App\Support\SampleData\FakerFactoryShim::class, \Faker\Factory::class);
|
||||
}
|
||||
if (! class_exists(\Faker\Generator::class, false)) {
|
||||
class_alias(\App\Support\SampleData\FakerShim::class, \Faker\Generator::class);
|
||||
}
|
||||
}
|
||||
|
||||
// Laravel 의 fake() 헬퍼는 framework/helpers.php 에서
|
||||
// `class_exists(\Faker\Factory::class)` 가 true 일 때만 정의됨.
|
||||
// Faker 미설치 시 Laravel 이 fake() 를 정의하지 못하므로 대체 구현 제공.
|
||||
if (! function_exists('fake')) {
|
||||
/**
|
||||
* FakerShim 기반 fake() 대체 구현
|
||||
*
|
||||
* @param string|null $locale 로케일 (기본: config('app.faker_locale') 또는 'ko_KR')
|
||||
* @return \Faker\Generator (class_alias 로 App\Support\SampleData\FakerShim 과 동일)
|
||||
*/
|
||||
function fake($locale = null)
|
||||
{
|
||||
if ($locale === null && function_exists('app') && app()->bound('config')) {
|
||||
$locale = app('config')->get('app.faker_locale', 'ko_KR');
|
||||
}
|
||||
|
||||
return \Faker\Factory::create($locale ?? 'ko_KR');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user