fix : 테넌트별 옵션설정 작업
- Tenant Fields - Tenant Option Groups - Tenant Option Values - Tenant Profiles
This commit is contained in:
213
app/Services/TenantFieldSettingService.php
Normal file
213
app/Services/TenantFieldSettingService.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\Models\Tenants\SettingFieldDef;
|
||||
use App\Models\Tenants\TenantFieldSetting;
|
||||
use App\Models\Tenants\TenantOptionGroup;
|
||||
|
||||
class TenantFieldSettingService
|
||||
{
|
||||
/** 활성 테넌트 ID 조회 (공통) */
|
||||
protected static function tenantId(): ?int
|
||||
{
|
||||
return app('tenant_id'); // 미들웨어에서 주입된 테넌트 ID 가정
|
||||
}
|
||||
|
||||
/** 효과값 1개 빌드: 전역정의 + 테넌트설정 merge */
|
||||
protected static function buildEffectiveRow(SettingFieldDef $def, ?TenantFieldSetting $s): array
|
||||
{
|
||||
$enabled = $s ? (bool)$s->enabled : (bool)$def->is_core;
|
||||
return [
|
||||
'field_key' => $def->field_key,
|
||||
'label' => $def->label,
|
||||
'data_type' => $def->data_type,
|
||||
'input_type' => $def->input_type,
|
||||
'option_source' => $def->option_source,
|
||||
'option_payload' => $def->option_payload,
|
||||
'storage_area' => $def->storage_area,
|
||||
'storage_key' => $def->storage_key,
|
||||
'is_core' => (bool)$def->is_core,
|
||||
// tenant overlay
|
||||
'enabled' => $enabled,
|
||||
'required' => $s ? (bool)$s->required : false,
|
||||
'sort_order' => $s ? (int)$s->sort_order : 0,
|
||||
'option_group_id' => $s ? $s->option_group_id : null,
|
||||
'code_group' => $s ? $s->code_group : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 효과값 목록
|
||||
* - 코어(is_core=1)는 기본 enabled=true
|
||||
* - enabled=false는 제외
|
||||
* - sort_order → field_key 순 정렬
|
||||
*/
|
||||
public static function index(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$defs = SettingFieldDef::query()
|
||||
->orderByDesc('is_core')
|
||||
->orderBy('field_key')
|
||||
->get()
|
||||
->keyBy('field_key');
|
||||
|
||||
$settings = TenantFieldSetting::where('tenant_id', $tenantId)
|
||||
->get()
|
||||
->keyBy('field_key');
|
||||
|
||||
$rows = [];
|
||||
foreach ($defs as $key => $def) {
|
||||
$s = $settings->get($key);
|
||||
$effective = self::buildEffectiveRow($def, $s);
|
||||
if ($effective['enabled'] !== true) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = $effective;
|
||||
}
|
||||
|
||||
usort($rows, fn ($a, $b) =>
|
||||
($a['sort_order'] <=> $b['sort_order']) ?: strcmp($a['field_key'], $b['field_key'])
|
||||
);
|
||||
|
||||
return ApiResponse::response('result', array_values($rows));
|
||||
}
|
||||
|
||||
/**
|
||||
* [PUT] 대량 업서트
|
||||
* params:
|
||||
* items: [{field_key, enabled?, required?, sort_order?, option_group_id?, code_group?}, ...]
|
||||
*/
|
||||
public static function bulkUpsert(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'items' => ['required','array','min:1'],
|
||||
'items.*.field_key' => ['required','string','max:64', Rule::exists('setting_field_defs','field_key')],
|
||||
'items.*.enabled' => ['nullable','boolean'],
|
||||
'items.*.required' => ['nullable','boolean'],
|
||||
'items.*.sort_order' => ['nullable','integer'],
|
||||
'items.*.option_group_id' => ['nullable','integer'],
|
||||
'items.*.code_group' => ['nullable','string','max:50'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$payload = $v->validated();
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($payload, $tenantId) {
|
||||
foreach ($payload['items'] as $row) {
|
||||
// option_group_id 유효성(해당 테넌트 소유인지)
|
||||
if (!empty($row['option_group_id'])) {
|
||||
$exists = TenantOptionGroup::where('tenant_id', $tenantId)
|
||||
->where('id', $row['option_group_id'])
|
||||
->exists();
|
||||
if (!$exists) {
|
||||
throw new \RuntimeException('option_group_id is invalid for this tenant');
|
||||
}
|
||||
}
|
||||
|
||||
TenantFieldSetting::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'field_key' => $row['field_key']],
|
||||
[
|
||||
'enabled' => isset($row['enabled']) ? (int)$row['enabled'] : 0,
|
||||
'required' => isset($row['required']) ? (int)$row['required'] : 0,
|
||||
'sort_order' => isset($row['sort_order']) ? (int)$row['sort_order'] : 0,
|
||||
'option_group_id' => $row['option_group_id'] ?? null,
|
||||
'code_group' => $row['code_group'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
return ApiResponse::error($e->getMessage(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return ApiResponse::error('저장 중 오류가 발생했습니다.', 500);
|
||||
}
|
||||
|
||||
return ApiResponse::response('result', ['updated' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 단건 수정
|
||||
* - 응답은 FieldEffective 포맷으로 반환
|
||||
*/
|
||||
public static function updateOne(string $fieldKey, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
// 전역 key 존재 확인
|
||||
$def = SettingFieldDef::where('field_key', $fieldKey)->first();
|
||||
if (!$def) {
|
||||
return ApiResponse::error('field_key not found', 404);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'enabled' => ['nullable','boolean'],
|
||||
'required' => ['nullable','boolean'],
|
||||
'sort_order' => ['nullable','integer'],
|
||||
'option_group_id' => ['nullable','integer'],
|
||||
'code_group' => ['nullable','string','max:50'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
$data = $v->validated();
|
||||
|
||||
// option_group_id 테넌트 소유 검증
|
||||
if (!empty($data['option_group_id'])) {
|
||||
$ok = TenantOptionGroup::where('tenant_id', $tenantId)
|
||||
->where('id', $data['option_group_id'])
|
||||
->exists();
|
||||
if (!$ok) {
|
||||
return ApiResponse::error('option_group_id is invalid for this tenant', 422);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$item = TenantFieldSetting::firstOrNew([
|
||||
'tenant_id' => $tenantId,
|
||||
'field_key' => $fieldKey,
|
||||
]);
|
||||
|
||||
// 제공된 키만 반영
|
||||
foreach (['enabled','required','sort_order','option_group_id','code_group'] as $k) {
|
||||
if (array_key_exists($k, $data)) {
|
||||
$item->{$k} = $data[$k];
|
||||
}
|
||||
}
|
||||
$item->save();
|
||||
|
||||
// 최신 설정 다시 로드하여 효과값으로 리턴
|
||||
$s = TenantFieldSetting::where('tenant_id', $tenantId)
|
||||
->where('field_key', $fieldKey)
|
||||
->first();
|
||||
|
||||
$effective = self::buildEffectiveRow($def, $s);
|
||||
|
||||
return ApiResponse::response('result', $effective);
|
||||
} catch (\Throwable $e) {
|
||||
return ApiResponse::error('수정 중 오류가 발생했습니다.', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
app/Services/TenantOptionGroupService.php
Normal file
185
app/Services/TenantOptionGroupService.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\Models\Tenants\TenantOptionGroup;
|
||||
use App\Models\Tenants\TenantOptionValue;
|
||||
use App\Models\Tenants\TenantFieldSetting;
|
||||
|
||||
class TenantOptionGroupService
|
||||
{
|
||||
/** 활성 테넌트 ID */
|
||||
protected static function tenantId(): ?int
|
||||
{
|
||||
return app('tenant_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 옵션 그룹 목록 (페이징)
|
||||
* params: page?, per_page?/size?, q?
|
||||
*/
|
||||
public static function index(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$per = (int)($params['per_page'] ?? $params['size'] ?? 20);
|
||||
$page = isset($params['page']) ? (int)$params['page'] : null;
|
||||
|
||||
$q = TenantOptionGroup::where('tenant_id', $tenantId)->orderBy('group_key');
|
||||
|
||||
if (!empty($params['q'])) {
|
||||
$kw = $params['q'];
|
||||
$q->where(function ($w) use ($kw) {
|
||||
$w->where('group_key', 'like', "%{$kw}%")
|
||||
->orWhere('name', 'like', "%{$kw}%");
|
||||
});
|
||||
}
|
||||
|
||||
$data = $q->paginate($per, ['*'], 'page', $page);
|
||||
|
||||
return ApiResponse::response('result', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* [POST] 옵션 그룹 생성
|
||||
*/
|
||||
public static function store(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'group_key' => [
|
||||
'required','string','max:64','alpha_dash',
|
||||
Rule::unique('tenant_option_groups', 'group_key')
|
||||
->where(fn ($q) => $q->where('tenant_id', $tenantId)),
|
||||
],
|
||||
'name' => ['required','string','max:100'],
|
||||
'description' => ['nullable','string','max:255'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$data = $v->validated();
|
||||
$data['tenant_id'] = $tenantId;
|
||||
|
||||
$item = TenantOptionGroup::create($data);
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 옵션 그룹 단건 조회
|
||||
*/
|
||||
public static function show(int $id)
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
if (!$id) {
|
||||
return ApiResponse::error('id가 올바르지 않습니다.', 422);
|
||||
}
|
||||
|
||||
$item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 옵션 그룹 수정
|
||||
*/
|
||||
public static function update(int $id, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
if (!$id) {
|
||||
return ApiResponse::error('id가 올바르지 않습니다.', 422);
|
||||
}
|
||||
|
||||
$item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'group_key' => [
|
||||
'sometimes','string','max:64','alpha_dash',
|
||||
Rule::unique('tenant_option_groups', 'group_key')
|
||||
->where(fn ($q) => $q->where('tenant_id', $tenantId))
|
||||
->ignore($item->id),
|
||||
],
|
||||
'name' => ['sometimes','string','max:100'],
|
||||
'description' => ['nullable','string','max:255'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$data = $v->validated();
|
||||
if (empty($data)) {
|
||||
return ApiResponse::error('수정할 항목이 없습니다.', 422);
|
||||
}
|
||||
|
||||
$item->fill($data)->save();
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [DELETE] 옵션 그룹 삭제
|
||||
* - 참조 무결성 검사:
|
||||
* 1) tenant_option_values에 값이 남아있으면 삭제 불가
|
||||
* 2) tenant_field_settings.option_group_id에서 참조 중이면 삭제 불가
|
||||
*/
|
||||
public static function destroy(int $id)
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
if (!$id) {
|
||||
return ApiResponse::error('id가 올바르지 않습니다.', 422);
|
||||
}
|
||||
|
||||
$item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
// 1) 옵션 값 존재 여부
|
||||
$hasValues = TenantOptionValue::where('group_id', $item->id)->exists();
|
||||
if ($hasValues) {
|
||||
return ApiResponse::error('해당 그룹에 옵션 값이 존재하여 삭제할 수 없습니다.', 409);
|
||||
}
|
||||
|
||||
// 2) 필드 설정에서 참조 여부
|
||||
$isReferenced = TenantFieldSetting::where('tenant_id', $tenantId)
|
||||
->where('option_group_id', $item->id)
|
||||
->exists();
|
||||
if ($isReferenced) {
|
||||
return ApiResponse::error('필드 설정에서 참조 중인 그룹은 삭제할 수 없습니다.', 409);
|
||||
}
|
||||
|
||||
$item->delete();
|
||||
|
||||
return ApiResponse::response('result', ['deleted' => true]);
|
||||
}
|
||||
}
|
||||
231
app/Services/TenantOptionValueService.php
Normal file
231
app/Services/TenantOptionValueService.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\Models\Tenants\TenantOptionGroup;
|
||||
use App\Models\Tenants\TenantOptionValue;
|
||||
|
||||
class TenantOptionValueService
|
||||
{
|
||||
/** 활성 테넌트 ID */
|
||||
protected static function tenantId(): ?int
|
||||
{
|
||||
return app('tenant_id');
|
||||
}
|
||||
|
||||
/** 테넌트 소유 그룹 로드 (없으면 null) */
|
||||
protected static function loadGroup(int $tenantId, int $groupId): ?TenantOptionGroup
|
||||
{
|
||||
return TenantOptionGroup::where('tenant_id', $tenantId)->find($groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 옵션 값 목록
|
||||
* params: active_only?(bool)
|
||||
*/
|
||||
public static function index(int $groupId, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$q = TenantOptionValue::where('group_id', $group->id)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('value_label');
|
||||
|
||||
if (!empty($params['active_only'])) {
|
||||
$q->where('is_active', 1);
|
||||
}
|
||||
|
||||
$list = $q->get();
|
||||
|
||||
return ApiResponse::response('result', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* [POST] 옵션 값 생성
|
||||
*/
|
||||
public static function store(int $groupId, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'value_key' => [
|
||||
'required','string','max:64','alpha_dash',
|
||||
Rule::unique('tenant_option_values', 'value_key')
|
||||
->where(fn ($q) => $q->where('group_id', $group->id)),
|
||||
],
|
||||
'value_label' => ['required','string','max:100'],
|
||||
'sort_order' => ['nullable','integer'],
|
||||
'is_active' => ['nullable','boolean'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$data = $v->validated();
|
||||
$data['group_id'] = $group->id;
|
||||
$item = TenantOptionValue::create($data);
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 옵션 값 단건 조회
|
||||
*/
|
||||
public static function show(int $groupId, int $id)
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$item = TenantOptionValue::where('group_id', $group->id)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 값을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 옵션 값 수정
|
||||
*/
|
||||
public static function update(int $groupId, int $id, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$item = TenantOptionValue::where('group_id', $group->id)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 값을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'value_key' => [
|
||||
'sometimes','string','max:64','alpha_dash',
|
||||
Rule::unique('tenant_option_values', 'value_key')
|
||||
->where(fn ($q) => $q->where('group_id', $group->id))
|
||||
->ignore($item->id),
|
||||
],
|
||||
'value_label' => ['sometimes','string','max:100'],
|
||||
'sort_order' => ['nullable','integer'],
|
||||
'is_active' => ['nullable','boolean'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$data = $v->validated();
|
||||
if (empty($data)) {
|
||||
return ApiResponse::error('수정할 항목이 없습니다.', 422);
|
||||
}
|
||||
|
||||
$item->fill($data)->save();
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [DELETE] 옵션 값 삭제
|
||||
* - TODO: 실제 프로필/필드설정에서 value_key 참조 여부 체크가 필요하면 여기서 차단
|
||||
*/
|
||||
public static function destroy(int $groupId, int $id)
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$item = TenantOptionValue::where('group_id', $group->id)->find($id);
|
||||
if (!$item) {
|
||||
return ApiResponse::error('옵션 값을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
// TODO: 참조 무결성 검사(필요 시 구현)
|
||||
$item->delete();
|
||||
|
||||
return ApiResponse::response('result', ['deleted' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 정렬 순서 일괄 변경
|
||||
* params: items: [{id, sort_order}, ...]
|
||||
*/
|
||||
public static function reorder(int $groupId, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$group = self::loadGroup($tenantId, $groupId);
|
||||
if (!$group) {
|
||||
return ApiResponse::error('옵션 그룹을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'items' => ['required','array','min:1'],
|
||||
'items.*.id' => ['required','integer'],
|
||||
'items.*.sort_order' => ['required','integer'],
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ApiResponse::error($v->errors()->first(), 422);
|
||||
}
|
||||
|
||||
$rows = $v->validated()['items'];
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($group, $rows) {
|
||||
foreach ($rows as $r) {
|
||||
TenantOptionValue::where('group_id', $group->id)
|
||||
->where('id', $r['id'])
|
||||
->update(['sort_order' => (int) $r['sort_order']]);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
return ApiResponse::error('정렬 순서 저장 중 오류가 발생했습니다.', 500);
|
||||
}
|
||||
|
||||
return ApiResponse::response('result', ['reordered' => true]);
|
||||
}
|
||||
}
|
||||
194
app/Services/TenantUserProfileService.php
Normal file
194
app/Services/TenantUserProfileService.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Models\Tenants\SettingFieldDef;
|
||||
use App\Models\Tenants\TenantFieldSetting;
|
||||
use App\Models\Tenants\TenantUserProfile;
|
||||
|
||||
class TenantUserProfileService
|
||||
{
|
||||
/** 활성 테넌트 ID */
|
||||
protected static function tenantId(): ?int
|
||||
{
|
||||
return app('tenant_id');
|
||||
}
|
||||
|
||||
/** 효과값 필드(Enabled) 맵: field_key => [def, setting] */
|
||||
protected static function effectiveFieldMap(int $tenantId): array
|
||||
{
|
||||
$defs = SettingFieldDef::all()->keyBy('field_key');
|
||||
$settings = TenantFieldSetting::where('tenant_id', $tenantId)->get()->keyBy('field_key');
|
||||
|
||||
$map = [];
|
||||
foreach ($defs as $key => $def) {
|
||||
$s = $settings->get($key);
|
||||
$enabled = $s ? (bool)$s->enabled : (bool)$def->is_core;
|
||||
if (!$enabled) continue;
|
||||
$map[$key] = ['def' => $def, 'setting' => $s];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 프로필 목록 (페이징)
|
||||
* params: per_page?, q?
|
||||
*/
|
||||
public static function index(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
|
||||
$per = (int)($params['per_page'] ?? $params['size'] ?? 20);
|
||||
$q = TenantUserProfile::where('tenant_id', $tenantId)
|
||||
->with(['user:id,name,email']);
|
||||
|
||||
if (!empty($params['q'])) {
|
||||
$kw = $params['q'];
|
||||
$q->where(function ($w) use ($kw) {
|
||||
$w->where('display_name', 'like', "%{$kw}%")
|
||||
->orWhere('json_extra->employee_no', 'like', "%{$kw}%");
|
||||
});
|
||||
}
|
||||
|
||||
$page = isset($params['page']) ? (int)$params['page'] : null;
|
||||
$data = $q->orderByDesc('id')->paginate($per, ['*'], 'page', $page);
|
||||
|
||||
return ApiResponse::response('result', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 프로필 단건 조회
|
||||
*/
|
||||
public static function show(int $userId)
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
if (!$userId) {
|
||||
return ApiResponse::error('userId가 올바르지 않습니다.', 422);
|
||||
}
|
||||
|
||||
$item = TenantUserProfile::where('tenant_id', $tenantId)
|
||||
->where('user_id', $userId)
|
||||
->with(['user:id,name,email'])
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
return ApiResponse::error('프로필을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
return ApiResponse::response('result', $item->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 관리자 수정: enabled 필드만 write-back
|
||||
* - 입력 payload는 field_key: value 포맷(예: {"position":"manager","employee_no":"A-001"})
|
||||
*/
|
||||
public static function update(int $userId, array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
if (!$userId) {
|
||||
return ApiResponse::error('userId가 올바르지 않습니다.', 422);
|
||||
}
|
||||
|
||||
// (선택) 간단 유효성: 최소 1개 키 존재
|
||||
if (empty($params) || !is_array($params)) {
|
||||
return ApiResponse::error('수정할 항목이 없습니다.', 422);
|
||||
}
|
||||
|
||||
$fields = self::effectiveFieldMap($tenantId);
|
||||
$profile = TenantUserProfile::firstOrCreate(['tenant_id' => $tenantId, 'user_id' => $userId]);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($fields, $profile, $params) {
|
||||
foreach ($fields as $key => $meta) {
|
||||
if (!array_key_exists($key, $params)) continue; // 입력이 없으면 스킵
|
||||
|
||||
$def = $meta['def'];
|
||||
$value = $params[$key];
|
||||
|
||||
switch ($def->storage_area) {
|
||||
case 'tenant_profile':
|
||||
if ($def->storage_key) {
|
||||
$profile->{$def->storage_key} = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tenant_profile_json':
|
||||
$jsonKey = $def->storage_key ?: $key;
|
||||
$extra = is_array($profile->json_extra) ? $profile->json_extra : (array) json_decode((string)$profile->json_extra, true);
|
||||
$extra[$jsonKey] = $value;
|
||||
$profile->json_extra = $extra;
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
// 정책상 기본 비권장: 무시
|
||||
// 필요 시 UsersService 등으로 화이트리스트 업데이트 구현
|
||||
break;
|
||||
}
|
||||
}
|
||||
$profile->save();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
return ApiResponse::error('프로필 저장 중 오류가 발생했습니다.', 500);
|
||||
}
|
||||
|
||||
$fresh = TenantUserProfile::where('tenant_id', $tenantId)
|
||||
->where('user_id', $userId)
|
||||
->with(['user:id,name,email'])
|
||||
->first();
|
||||
|
||||
return ApiResponse::response('result', $fresh ? $fresh->toArray() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* [GET] 내 프로필 조회
|
||||
*/
|
||||
public static function me(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
$userId = auth()->id();
|
||||
if (!$userId) {
|
||||
return ApiResponse::error('인증 정보가 없습니다.', 401);
|
||||
}
|
||||
|
||||
$item = TenantUserProfile::where('tenant_id', $tenantId)
|
||||
->where('user_id', $userId)
|
||||
->with(['user:id,name,email'])
|
||||
->first();
|
||||
|
||||
// 없으면 null 반환(스펙에 따라 404로 바꾸려면 위와 동일 처리)
|
||||
return ApiResponse::response('result', $item ? $item->toArray() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* [PATCH] 내 프로필 수정
|
||||
* - enabled 필드만 반영 (관리자 수정과 동일 로직)
|
||||
*/
|
||||
public static function updateMe(array $params = [])
|
||||
{
|
||||
$tenantId = self::tenantId();
|
||||
if (!$tenantId) {
|
||||
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
|
||||
}
|
||||
$userId = auth()->id();
|
||||
if (!$userId) {
|
||||
return ApiResponse::error('인증 정보가 없습니다.', 401);
|
||||
}
|
||||
return self::update($userId, $params);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user