v7.0.0-beta.2 release

This commit is contained in:
HeuJung
2026-04-20 20:37:49 +09:00
parent 6595fd0eb5
commit fa8eba7a0d
1165 changed files with 107118 additions and 762127 deletions
@@ -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);
}
}
+643
View File
@@ -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 를 설치하세요.'
);
}
}
+37
View File
@@ -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;
}
}
+48
View File
@@ -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} 회 재시도 후에도 고유 값을 생성하지 못했습니다. ".
'데이터셋 풀이 너무 작거나 요청 수가 데이터셋보다 많은지 확인하세요.'
);
}
}
+50
View File
@@ -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');
}
}