2025-08-14 17:20:28 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
|
|
|
use App\Models\Members\UserTenant;
|
|
|
|
|
|
|
|
|
|
class TenantService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 한글 자음 배열
|
|
|
|
|
*/
|
|
|
|
|
private const INITIALS = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 대소문자를 구분하지 않는 36진수 문자열로 변경
|
|
|
|
|
*/
|
|
|
|
|
private string $base36Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 한글 업체명에서 초성 약어를 추출합니다.
|
|
|
|
|
* 외부 라이브러리 없이 자체적으로 구현했습니다.
|
|
|
|
|
*
|
|
|
|
|
* @param string $tenantName
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function getInitials(string $tenantName): string
|
|
|
|
|
{
|
|
|
|
|
$initials = '';
|
|
|
|
|
$charLength = mb_strlen($tenantName, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
for ($i = 0; $i < $charLength; $i++) {
|
|
|
|
|
$char = mb_substr($tenantName, $i, 1, 'UTF-8');
|
|
|
|
|
$code = mb_ord($char, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
// 한글 초성을 추출
|
|
|
|
|
if ($code >= 0xAC00 && $code <= 0xD7A3) { // 한글 유니코드 범위
|
|
|
|
|
$index = floor(($code - 0xAC00) / 588);
|
|
|
|
|
$initials .= self::INITIALS[$index];
|
|
|
|
|
}
|
|
|
|
|
// 한글이 아닌 문자는 무시합니다.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$koreanInitials = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
|
|
|
|
$englishInitials = ['G','KK','N','D','TT','R','M','B','BB','S','SS','O','J','JJ','CH','K','T','P','H'];
|
|
|
|
|
$initials = strtr($initials, array_combine($koreanInitials, $englishInitials));
|
|
|
|
|
|
|
|
|
|
$initials = str_replace(' ', '', $initials);
|
|
|
|
|
return strtoupper($initials);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 10진수 숫자를 4자리 36진수 문자열로 변환합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param int $number
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function toBase36(int $number): string
|
|
|
|
|
{
|
|
|
|
|
$result = '';
|
|
|
|
|
$base = strlen($this->base36Chars);
|
|
|
|
|
|
|
|
|
|
// **수정된 부분: 4자리 고정**
|
|
|
|
|
for ($i = 0; $i < 4; $i++) {
|
|
|
|
|
$remainder = $number % $base;
|
|
|
|
|
$result = $this->base36Chars[$remainder] . $result;
|
|
|
|
|
$number = floor($number / $base);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 36진수 문자열을 10진수 숫자로 변환합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param string $base36String
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
private function fromBase36(string $base36String): int
|
|
|
|
|
{
|
|
|
|
|
$number = 0;
|
|
|
|
|
$base = strlen($this->base36Chars);
|
|
|
|
|
$len = strlen($base36String);
|
|
|
|
|
|
|
|
|
|
for ($i = 0; $i < $len; $i++) {
|
|
|
|
|
$char = $base36String[$i];
|
|
|
|
|
$charValue = strpos($this->base36Chars, $char);
|
|
|
|
|
|
|
|
|
|
// strpos가 false를 반환할 경우를 대비해 예외 처리
|
|
|
|
|
if ($charValue === false) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
$number += $charValue * ($base ** ($len - 1 - $i));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 한글 업체명 기반으로 순환형 테넌트 코드를 생성합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param string $tenantName
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function generateTenantCode(string $tenantName): string
|
|
|
|
|
{
|
|
|
|
|
$cleanTenantName = str_replace(['주식회사', '(주)', '유한회사', '(유)', '유한책임회사', '(유)책', '합명회사', '(합)', '합자회사', '(합자)'], '', $tenantName);
|
|
|
|
|
|
|
|
|
|
// 1. 전처리된 업체명에서 초성 약어 생성
|
|
|
|
|
$initials = $this->getInitials($cleanTenantName);
|
|
|
|
|
|
|
|
|
|
// 2. 모든 테넌트들의 코드 중 가장 큰 순번을 찾습니다.
|
|
|
|
|
$lastNumber = -1;
|
|
|
|
|
$existingTenants = Tenant::all();
|
|
|
|
|
|
|
|
|
|
foreach ($existingTenants as $tenant) {
|
|
|
|
|
// **수정된 부분: 코드 마지막 4자리를 순번으로 간주**
|
|
|
|
|
if (strlen($tenant->code) >= 4) {
|
|
|
|
|
$sequenceString = substr($tenant->code, -4);
|
|
|
|
|
|
|
|
|
|
// 마지막 4자리가 36진수 문자열인지 확인
|
|
|
|
|
if (strspn($sequenceString, $this->base36Chars) === 4) {
|
|
|
|
|
$sequence = $this->fromBase36($sequenceString);
|
|
|
|
|
if ($sequence > $lastNumber) {
|
|
|
|
|
$lastNumber = $sequence;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 마지막 순번에 1을 더하고 36^4 (1,679,616)로 나눈 나머지로 순환하도록 유지합니다.
|
|
|
|
|
// **수정된 부분: 46656 -> 1679616 으로 변경**
|
|
|
|
|
$nextSequence = ($lastNumber + 1) % 1679616;
|
|
|
|
|
|
|
|
|
|
// 4. 순번을 4자리 36진수 문자열로 포맷
|
|
|
|
|
// **수정된 부분: toBase36 호출**
|
|
|
|
|
$formattedSequence = $this->toBase36($nextSequence);
|
|
|
|
|
|
|
|
|
|
// 5. 초성 약어와 순번을 조합하여 최종 코드 생성
|
|
|
|
|
$code = $initials . $formattedSequence;
|
|
|
|
|
|
|
|
|
|
return $code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테넌트 목록 조회 (페이징)
|
|
|
|
|
*
|
|
|
|
|
* @param array $params [page, size, search 등]
|
|
|
|
|
*/
|
|
|
|
|
public static function getTenants(array $params = [])
|
|
|
|
|
{
|
2025-08-20 11:02:46 +09:00
|
|
|
$tenantId = $params['tenant_id'] ?? app('tenant_id');
|
|
|
|
|
|
|
|
|
|
if (!$tenantId) {
|
|
|
|
|
// 현재 사용자 기본 테넌트 조회
|
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
|
$userTenant = UserTenant::where('user_id', $apiUser)
|
|
|
|
|
->where('is_default', 1)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$userTenant) {
|
|
|
|
|
return ['error' => '활성(기본) 테넌트를 찾을 수 없습니다.', 'code' => 404];
|
|
|
|
|
}
|
|
|
|
|
$tenantId = $userTenant->tenant_id;
|
|
|
|
|
}
|
2025-08-14 17:20:28 +09:00
|
|
|
|
|
|
|
|
$pageNo = isset($params['page']) ? (int)$params['page'] : 1;
|
|
|
|
|
$pageSize = isset($params['size']) ? (int)$params['size'] : 10;
|
|
|
|
|
|
|
|
|
|
$query = Tenant::query();
|
|
|
|
|
|
|
|
|
|
// (옵션) 간단 검색 예시: 회사명/코드
|
|
|
|
|
if (!empty($params['q'])) {
|
|
|
|
|
$q = trim($params['q']);
|
|
|
|
|
$query->where(function ($qq) use ($q) {
|
|
|
|
|
$qq->where('company_name', 'like', "%{$q}%")
|
|
|
|
|
->orWhere('code', 'like', "%{$q}%")
|
|
|
|
|
->orWhere('email', 'like', "%{$q}%");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// (옵션) 정렬
|
|
|
|
|
if (!empty($params['sort']) && in_array($params['sort'], ['company_name','code','created_at','updated_at'])) {
|
|
|
|
|
$dir = (!empty($params['dir']) && in_array(strtolower($params['dir']), ['asc','desc'])) ? $params['dir'] : 'desc';
|
|
|
|
|
$query->orderBy($params['sort'], $dir);
|
|
|
|
|
} else {
|
|
|
|
|
$query->orderByDesc('id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$paginator = $query->paginate($pageSize, ['*'], 'page', $pageNo);
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $paginator;
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 단일 테넌트 조회
|
|
|
|
|
* - params.tenant_id 가 있으면 해당 테넌트
|
|
|
|
|
* - 없으면 현재 사용자 기본(is_default=1) 테넌트
|
|
|
|
|
*
|
|
|
|
|
* @param array $params [tenant_id]
|
|
|
|
|
*/
|
|
|
|
|
public static function getTenant(array $params = [])
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$tenantId = $params['tenant_id'] ?? app('tenant_id');
|
|
|
|
|
|
|
|
|
|
if (!$tenantId) {
|
|
|
|
|
// 현재 사용자 기본 테넌트 조회
|
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
|
$userTenant = UserTenant::where('user_id', $apiUser)
|
|
|
|
|
->where('is_default', 1)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$userTenant) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '활성(기본) 테넌트를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
$tenantId = $userTenant->tenant_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 필요한 컬럼만 선택 (원하면 조정)
|
|
|
|
|
$query = Tenant::query()
|
|
|
|
|
->select('id','company_name','code','email','phone','address','business_num','corp_reg_no','ceo_name','homepage','fax','logo','admin_memo','options','created_at','updated_at')
|
|
|
|
|
->where('id', $tenantId);
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $query->first();
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테넌트 등록
|
|
|
|
|
*
|
|
|
|
|
* @param array $params
|
|
|
|
|
*/
|
|
|
|
|
public static function storeTenants(array $params = [])
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$validator = Validator::make($params, [
|
|
|
|
|
'company_name' => 'required|string|max:255',
|
|
|
|
|
'email' => 'nullable|email|max:100',
|
|
|
|
|
'phone' => 'nullable|string|max:30',
|
|
|
|
|
'address' => 'nullable|string|max:255',
|
|
|
|
|
'business_num' => 'nullable|string|max:30',
|
|
|
|
|
'corp_reg_no' => 'nullable|string|max:30',
|
|
|
|
|
'ceo_name' => 'nullable|string|max:100',
|
|
|
|
|
'homepage' => 'nullable|string|max:255',
|
|
|
|
|
'fax' => 'nullable|string|max:50',
|
|
|
|
|
'logo' => 'nullable|string|max:255',
|
|
|
|
|
'admin_memo' => 'nullable|string',
|
|
|
|
|
'options' => 'nullable', // JSON 문자열 저장이라면 'nullable|json'
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => $validator->errors()->first(), 'code' => 400];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload = $validator->validated();
|
|
|
|
|
|
|
|
|
|
// TenantService 인스턴스를 가져옵니다.
|
|
|
|
|
$tenantService = app(TenantService::class);
|
|
|
|
|
|
|
|
|
|
// 업체명 기반으로 고유한 코드를 생성합니다.
|
|
|
|
|
$code = $tenantService->generateTenantCode($payload['company_name']);
|
|
|
|
|
|
|
|
|
|
// 생성된 코드를 페이로드에 추가합니다.
|
|
|
|
|
$payload['code'] = $code;
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::create($payload);
|
|
|
|
|
|
2025-08-15 16:32:11 +09:00
|
|
|
// 기존 기본값(is_default=1) 해제
|
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
|
UserTenant::withoutGlobalScopes()
|
|
|
|
|
->where('user_id', $apiUser)
|
|
|
|
|
->where('is_default', 1)
|
|
|
|
|
->update(['is_default' => 0]);
|
|
|
|
|
|
|
|
|
|
|
2025-08-14 20:19:51 +09:00
|
|
|
// 성성된 테넌트를 나의 테넌트로 셋팅
|
|
|
|
|
$apiUser = app('api_user');
|
|
|
|
|
UserTenant::create([
|
|
|
|
|
'user_id' => $apiUser,
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
'is_active' => 0,
|
|
|
|
|
'is_default' => 1,
|
|
|
|
|
'joined_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-08-14 17:20:28 +09:00
|
|
|
// 생성된 리소스를 그대로 반환 (목록 카드용 요약 원하면 컬럼 제한)
|
2025-08-19 12:41:17 +09:00
|
|
|
return $tenant;
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테넌트 수정
|
|
|
|
|
*
|
|
|
|
|
* @param array $params
|
|
|
|
|
*/
|
|
|
|
|
public static function updateTenant(array $params = [])
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$validator = Validator::make($params, [
|
|
|
|
|
'company_name' => 'sometimes|string|max:255',
|
|
|
|
|
'email' => 'sometimes|nullable|email|max:100',
|
|
|
|
|
'phone' => 'sometimes|nullable|string|max:30',
|
|
|
|
|
'address' => 'sometimes|nullable|string|max:255',
|
|
|
|
|
'business_num' => 'sometimes|nullable|string|max:30',
|
|
|
|
|
'corp_reg_no' => 'sometimes|nullable|string|max:30',
|
|
|
|
|
'ceo_name' => 'sometimes|nullable|string|max:100',
|
|
|
|
|
'homepage' => 'sometimes|nullable|string|max:255',
|
|
|
|
|
'fax' => 'sometimes|nullable|string|max:50',
|
|
|
|
|
'logo' => 'sometimes|nullable|string|max:255',
|
|
|
|
|
'admin_memo' => 'sometimes|nullable|string',
|
|
|
|
|
'options' => 'sometimes|nullable', // JSON 문자열이면 'sometimes|nullable|json'
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => $validator->errors()->first(), 'code' => 400];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload = $validator->validated();
|
|
|
|
|
$tenantId = app('tenant_id') ?? null;
|
|
|
|
|
unset($payload['tenant_id']);
|
|
|
|
|
|
|
|
|
|
if (empty($payload)) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '수정할 데이터가 없습니다.', 'code' => 400];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::find($tenantId);
|
|
|
|
|
if (!$tenant) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenant->update($payload);
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return $tenant->fresh();
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테넌트 삭제(탈퇴) — 소프트 삭제 가정
|
|
|
|
|
*
|
|
|
|
|
* @param int $tenant_id
|
|
|
|
|
*/
|
|
|
|
|
public static function destroyTenant(array $params = [])
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $params['tenant_id'] ?? app('tenant_id');
|
|
|
|
|
if (!$tenantId) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => 'tenant_id가 필요합니다.', 'code' => 400];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::find($tenantId);
|
|
|
|
|
if (!$tenant) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenant->delete(); // SoftDeletes 트레이트가 있으면 소프트 삭제
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return 'success';
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테넌트 복구 (소프트 삭제된 레코드 대상)
|
|
|
|
|
*
|
|
|
|
|
* @param array $params [tenant_id:int]
|
|
|
|
|
*/
|
|
|
|
|
public static function restoreTenant(array $params = [])
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $params['tenant_id'] ?? app('tenant_id');
|
|
|
|
|
|
|
|
|
|
// 소프트 삭제 포함 조회
|
|
|
|
|
$tenant = Tenant::withTrashed()->find($tenantId);
|
|
|
|
|
if (!$tenant) {
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_null($tenant->deleted_at)) {
|
|
|
|
|
// 이미 활성 상태
|
2025-08-19 12:41:17 +09:00
|
|
|
return ['error' => '이미 활성화된 테넌트입니다.', 'code' => 400];
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenant->restore();
|
|
|
|
|
|
|
|
|
|
// 복구 결과를 data에 담고 싶으면 fresh() 후 필요한 필드만 반환
|
2025-08-19 12:41:17 +09:00
|
|
|
// return $tenant->fresh();
|
2025-08-14 17:20:28 +09:00
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
return 'success';
|
2025-08-14 17:20:28 +09:00
|
|
|
}
|
|
|
|
|
}
|