fix : 테넌트별 옵션설정 작업
- Tenant Fields - Tenant Option Groups - Tenant Option Values - Tenant Profiles
This commit is contained in:
35
app/Http/Controllers/Api/V1/TenantFieldSettingController.php
Normal file
35
app/Http/Controllers/Api/V1/TenantFieldSettingController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\TenantFieldSettingService;
|
||||
use App\Helpers\ApiResponse;
|
||||
|
||||
class TenantFieldSettingController extends Controller
|
||||
{
|
||||
// GET /v1/fields
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantFieldSettingService::index($request->all());
|
||||
}, '테넌트 필드 효과값 목록 조회');
|
||||
}
|
||||
|
||||
// PUT /v1/fields/bulk
|
||||
public function bulkUpsert(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantFieldSettingService::bulkUpsert($request->all());
|
||||
}, '테넌트 필드 설정 대량 저장');
|
||||
}
|
||||
|
||||
// PATCH /v1/fields/{key}
|
||||
public function updateOne(Request $request, string $key)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $key) {
|
||||
return TenantFieldSettingService::updateOne($key, $request->all());
|
||||
}, '테넌트 필드 설정 단건 수정');
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Api/V1/TenantOptionGroupController.php
Normal file
51
app/Http/Controllers/Api/V1/TenantOptionGroupController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\TenantOptionGroupService;
|
||||
use App\Helpers\ApiResponse;
|
||||
|
||||
class TenantOptionGroupController extends Controller
|
||||
{
|
||||
// GET /v1/opt-groups
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantOptionGroupService::index($request->all());
|
||||
}, '옵션 그룹 목록 조회');
|
||||
}
|
||||
|
||||
// POST /v1/opt-groups
|
||||
public function store(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantOptionGroupService::store($request->all());
|
||||
}, '옵션 그룹 생성');
|
||||
}
|
||||
|
||||
// GET /v1/opt-groups/{id}
|
||||
public function show($id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return TenantOptionGroupService::show($id);
|
||||
}, '옵션 그룹 단건 조회');
|
||||
}
|
||||
|
||||
// PATCH /v1/opt-groups/{id}
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return TenantOptionGroupService::update($id, $request->all());
|
||||
}, '옵션 그룹 수정');
|
||||
}
|
||||
|
||||
// DELETE /v1/opt-groups/{id}
|
||||
public function destroy($id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return TenantOptionGroupService::destroy($id);
|
||||
}, '옵션 그룹 삭제');
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/Api/V1/TenantOptionValueController.php
Normal file
59
app/Http/Controllers/Api/V1/TenantOptionValueController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\TenantOptionValueService;
|
||||
use App\Helpers\ApiResponse;
|
||||
|
||||
class TenantOptionValueController extends Controller
|
||||
{
|
||||
// GET /v1/opt-groups/{gid}/values
|
||||
public function index(Request $request, $gid)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $gid) {
|
||||
return TenantOptionValueService::index($gid, $request->all());
|
||||
}, '옵션 값 목록 조회');
|
||||
}
|
||||
|
||||
// POST /v1/opt-groups/{gid}/values
|
||||
public function store(Request $request, $gid)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $gid) {
|
||||
return TenantOptionValueService::store($gid, $request->all());
|
||||
}, '옵션 값 생성');
|
||||
}
|
||||
|
||||
// GET /v1/opt-groups/{gid}/values/{id}
|
||||
public function show($gid, $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($gid, $id) {
|
||||
return TenantOptionValueService::show($gid, $id);
|
||||
}, '옵션 값 단건 조회');
|
||||
}
|
||||
|
||||
// PATCH /v1/opt-groups/{gid}/values/{id}
|
||||
public function update(Request $request, $gid, $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $gid, $id) {
|
||||
return TenantOptionValueService::update($gid, $id, $request->all());
|
||||
}, '옵션 값 수정');
|
||||
}
|
||||
|
||||
// DELETE /v1/opt-groups/{gid}/values/{id}
|
||||
public function destroy($gid, $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($gid, $id) {
|
||||
return TenantOptionValueService::destroy($gid, $id);
|
||||
}, '옵션 값 삭제');
|
||||
}
|
||||
|
||||
// PATCH /v1/opt-groups/{gid}/values/reorder
|
||||
public function reorder(Request $request, $gid)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $gid) {
|
||||
return TenantOptionValueService::reorder($gid, $request->all());
|
||||
}, '옵션 값 정렬 순서 변경');
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Api/V1/TenantUserProfileController.php
Normal file
51
app/Http/Controllers/Api/V1/TenantUserProfileController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\TenantUserProfileService;
|
||||
use App\Helpers\ApiResponse;
|
||||
|
||||
class TenantUserProfileController extends Controller
|
||||
{
|
||||
// GET /v1/profiles
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantUserProfileService::index($request->all());
|
||||
}, '회원 프로필 목록 조회');
|
||||
}
|
||||
|
||||
// GET /v1/profiles/{userId}
|
||||
public function show($userId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($userId) {
|
||||
return TenantUserProfileService::show($userId);
|
||||
}, '회원 프로필 단건 조회');
|
||||
}
|
||||
|
||||
// PATCH /v1/profiles/{userId}
|
||||
public function update(Request $request, $userId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $userId) {
|
||||
return TenantUserProfileService::update($userId, $request->all());
|
||||
}, '회원 프로필 수정(관리자)');
|
||||
}
|
||||
|
||||
// GET /v1/profiles/me
|
||||
public function me(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantUserProfileService::me($request->all());
|
||||
}, '내 프로필 조회');
|
||||
}
|
||||
|
||||
// PATCH /v1/profiles/me
|
||||
public function updateMe(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return TenantUserProfileService::updateMe($request->all());
|
||||
}, '내 프로필 수정');
|
||||
}
|
||||
}
|
||||
34
app/Models/Tenants/SettingFieldDef.php
Normal file
34
app/Models/Tenants/SettingFieldDef.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SettingFieldDef extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'option_payload' => 'array',
|
||||
'is_core' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'field_key',
|
||||
'label',
|
||||
'data_type',
|
||||
'input_type',
|
||||
'option_source',
|
||||
'option_payload',
|
||||
'comment',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'storage_area',
|
||||
'storage_key',
|
||||
'is_core',
|
||||
];
|
||||
|
||||
// 관계 (테넌트 설정)
|
||||
public function tenantSettings()
|
||||
{
|
||||
return $this->hasMany(TenantFieldSetting::class, 'field_key', 'field_key');
|
||||
}
|
||||
}
|
||||
35
app/Models/Tenants/TenantFieldSetting.php
Normal file
35
app/Models/Tenants/TenantFieldSetting.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantFieldSetting extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
'required' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'field_key',
|
||||
'enabled',
|
||||
'required',
|
||||
'sort_order',
|
||||
'option_group_id',
|
||||
'code_group',
|
||||
];
|
||||
|
||||
public function fieldDef()
|
||||
{
|
||||
return $this->belongsTo(SettingFieldDef::class, 'field_key', 'field_key');
|
||||
}
|
||||
|
||||
public function optionGroup()
|
||||
{
|
||||
return $this->belongsTo(TenantOptionGroup::class, 'option_group_id');
|
||||
}
|
||||
}
|
||||
22
app/Models/Tenants/TenantOptionGroup.php
Normal file
22
app/Models/Tenants/TenantOptionGroup.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantOptionGroup extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'group_key',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function values()
|
||||
{
|
||||
return $this->hasMany(TenantOptionValue::class, 'group_id');
|
||||
}
|
||||
}
|
||||
27
app/Models/Tenants/TenantOptionValue.php
Normal file
27
app/Models/Tenants/TenantOptionValue.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantOptionValue extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'value_key',
|
||||
'value_label',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(TenantOptionGroup::class, 'group_id');
|
||||
}
|
||||
}
|
||||
42
app/Models/Tenants/TenantUserProfile.php
Normal file
42
app/Models/Tenants/TenantUserProfile.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Commons\Department;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantUserProfile extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'json_extra' => 'array',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'user_id',
|
||||
'department_id',
|
||||
'position_key',
|
||||
'job_title_key',
|
||||
'work_location_key',
|
||||
'employment_type_key',
|
||||
'manager_user_id',
|
||||
'json_extra',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'profile_photo_path',
|
||||
'display_name',
|
||||
];
|
||||
|
||||
// 관계: users 테이블은 전역이라 App\Models\User 로 연결
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
// 조직, 직급 등은 옵션/코드 참조 가능 (필요시 추가)
|
||||
public function department()
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
572
app/Swagger/v1/FieldProfileApi.php
Normal file
572
app/Swagger/v1/FieldProfileApi.php
Normal file
@@ -0,0 +1,572 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* 전역 태그 선언
|
||||
*
|
||||
* @OA\Tag(name="Tenant.Fields", description="테넌트별 필드 구동(효과값 조회/설정)")
|
||||
* @OA\Tag(name="Tenant.Option Groups", description="테넌트 관리형 옵션 그룹")
|
||||
* @OA\Tag(name="Tenant.Option Values", description="옵션 그룹의 항목(값) 관리")
|
||||
* @OA\Tag(name="Tenant.Profiles", description="테넌트별 회원 프로필 조회/수정")
|
||||
*/
|
||||
class FieldProfileApi
|
||||
{
|
||||
/**
|
||||
* Fields(효과값/설정) 스키마
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="FieldEffective",
|
||||
* type="object",
|
||||
* description="전역정의 + 테넌트설정이 merge된 필드 효과값",
|
||||
* required={"field_key","label","data_type","input_type","enabled"},
|
||||
* @OA\Property(property="field_key", type="string", example="position"),
|
||||
* @OA\Property(property="label", type="string", example="직급"),
|
||||
* @OA\Property(property="data_type", type="string", example="string"),
|
||||
* @OA\Property(property="input_type", type="string", example="select"),
|
||||
* @OA\Property(property="option_source", type="string", nullable=true, example="tenant_list"),
|
||||
* @OA\Property(property="option_payload", type="object", nullable=true),
|
||||
* @OA\Property(property="storage_area", type="string", example="tenant_profile"),
|
||||
* @OA\Property(property="storage_key", type="string", example="position_key"),
|
||||
* @OA\Property(property="is_core", type="boolean", example=false),
|
||||
* @OA\Property(property="enabled", type="boolean", example=true),
|
||||
* @OA\Property(property="required", type="boolean", example=false),
|
||||
* @OA\Property(property="sort_order", type="integer", example=10),
|
||||
* @OA\Property(property="option_group_id", type="integer", nullable=true, example=5),
|
||||
* @OA\Property(property="code_group", type="string", nullable=true, example="employment_type")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="FieldSettingUpdateRequest",
|
||||
* type="object",
|
||||
* @OA\Property(property="enabled", type="boolean", example=true),
|
||||
* @OA\Property(property="required", type="boolean", example=false),
|
||||
* @OA\Property(property="sort_order", type="integer", example=20),
|
||||
* @OA\Property(property="option_group_id", type="integer", nullable=true, example=5),
|
||||
* @OA\Property(property="code_group", type="string", nullable=true, example="department")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="FieldSettingBulkItem",
|
||||
* type="object",
|
||||
* required={"field_key"},
|
||||
* @OA\Property(property="field_key", type="string", example="position"),
|
||||
* @OA\Property(property="enabled", type="boolean", example=true),
|
||||
* @OA\Property(property="required", type="boolean", example=false),
|
||||
* @OA\Property(property="sort_order", type="integer", example=10),
|
||||
* @OA\Property(property="option_group_id", type="integer", nullable=true, example=5),
|
||||
* @OA\Property(property="code_group", type="string", nullable=true, example="employment_type")
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="FieldSettingBulkRequest",
|
||||
* type="object",
|
||||
* required={"items"},
|
||||
* @OA\Property(
|
||||
* property="items",
|
||||
* type="array",
|
||||
* @OA\Items(ref="#/components/schemas/FieldSettingBulkItem")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function _fieldSchemasNoop() {}
|
||||
|
||||
/**
|
||||
* Option Group/Value 스키마
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OptionGroup",
|
||||
* type="object",
|
||||
* required={"id","group_key","name"},
|
||||
* @OA\Property(property="id", type="integer", example=3),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="group_key", type="string", example="position"),
|
||||
* @OA\Property(property="name", type="string", example="직급표(2025)"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="본사 기준")
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="OptionGroupCreateRequest",
|
||||
* type="object",
|
||||
* required={"group_key","name"},
|
||||
* @OA\Property(property="group_key", type="string", example="job_title"),
|
||||
* @OA\Property(property="name", type="string", example="직책"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="영업조직용")
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="OptionGroupUpdateRequest",
|
||||
* type="object",
|
||||
* @OA\Property(property="group_key", type="string", example="job_title"),
|
||||
* @OA\Property(property="name", type="string", example="직책(개정)"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="영업/CS 통합")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="OptionValue",
|
||||
* type="object",
|
||||
* required={"id","group_id","value_key","value_label"},
|
||||
* @OA\Property(property="id", type="integer", example=10),
|
||||
* @OA\Property(property="group_id", type="integer", example=3),
|
||||
* @OA\Property(property="value_key", type="string", example="manager"),
|
||||
* @OA\Property(property="value_label", type="string", example="과장"),
|
||||
* @OA\Property(property="sort_order", type="integer", example=30),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="OptionValueCreateRequest",
|
||||
* type="object",
|
||||
* required={"value_key","value_label"},
|
||||
* @OA\Property(property="value_key", type="string", example="director"),
|
||||
* @OA\Property(property="value_label", type="string", example="이사"),
|
||||
* @OA\Property(property="sort_order", type="integer", example=40),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="OptionValueUpdateRequest",
|
||||
* type="object",
|
||||
* @OA\Property(property="value_key", type="string", example="director"),
|
||||
* @OA\Property(property="value_label", type="string", example="이사"),
|
||||
* @OA\Property(property="sort_order", type="integer", example=45),
|
||||
* @OA\Property(property="is_active", type="boolean", example=false)
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="OptionValueReorderRequest",
|
||||
* type="object",
|
||||
* required={"items"},
|
||||
* @OA\Property(
|
||||
* property="items",
|
||||
* type="array",
|
||||
* description="정렬 대상 목록",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* required={"id","sort_order"},
|
||||
* @OA\Property(property="id", type="integer", example=10),
|
||||
* @OA\Property(property="sort_order", type="integer", example=100)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function _optionSchemasNoop() {}
|
||||
|
||||
/**
|
||||
* Profiles 스키마
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Profile",
|
||||
* type="object",
|
||||
* description="테넌트별 사용자 프로필",
|
||||
* required={"tenant_id","user_id"},
|
||||
* @OA\Property(property="id", type="integer", example=101),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="user_id", type="integer", example=55),
|
||||
* @OA\Property(property="department_id", type="integer", nullable=true, example=12),
|
||||
* @OA\Property(property="position_key", type="string", nullable=true, example="manager"),
|
||||
* @OA\Property(property="job_title_key", type="string", nullable=true, example="lead_pm"),
|
||||
* @OA\Property(property="work_location_key", type="string", nullable=true, example="seoul_hq"),
|
||||
* @OA\Property(property="employment_type_key", type="string", nullable=true, example="regular"),
|
||||
* @OA\Property(property="manager_user_id", type="integer", nullable=true, example=77),
|
||||
* @OA\Property(property="display_name", type="string", nullable=true, example="김철수(영업1팀)"),
|
||||
* @OA\Property(property="profile_photo_path", type="string", nullable=true, example="/uploads/tenant/1/avatar/55.png"),
|
||||
* @OA\Property(property="json_extra", type="object", nullable=true,
|
||||
* example={"employee_no":"A-001","entry_date":"2023-01-02","work_type":"hybrid"})
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="ProfileUpdateRequest",
|
||||
* type="object",
|
||||
* description="프로필 수정 시, field_key: value 형태",
|
||||
* example={
|
||||
* "position":"manager",
|
||||
* "employee_no":"A-001",
|
||||
* "entry_date":"2023-01-02",
|
||||
* "work_type":"hybrid"
|
||||
* }
|
||||
* )
|
||||
* @OA\Schema(
|
||||
* schema="ProfilePagination",
|
||||
* type="object",
|
||||
* description="LengthAwarePaginator 구조",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Profile")),
|
||||
* @OA\Property(property="total", type="integer", example=3),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="path", type="string", example="/api/v1/profiles")
|
||||
* )
|
||||
*/
|
||||
public function _profileSchemasNoop() {}
|
||||
|
||||
/* =========================
|
||||
====== Endpoints ========
|
||||
========================= */
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/fields",
|
||||
* summary="효과값 필드 목록",
|
||||
* description="전역 필드 정의와 테넌트 설정을 병합한, 실제 화면 노출용(Enabled) 필드 목록을 반환합니다.",
|
||||
* tags={"Tenant.Fields"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array",
|
||||
* @OA\Items(ref="#/components/schemas/FieldEffective")
|
||||
* ))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function fieldsIndex() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/fields/bulk",
|
||||
* summary="필드 설정 대량 저장",
|
||||
* description="여러 필드에 대해 enabled/required/sort_order/option_group_id/code_group 설정을 한번에 저장합니다.",
|
||||
* tags={"Tenant.Fields"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/FieldSettingBulkRequest")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="저장 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="object", example={"updated":true}))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=400, description="유효성 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function fieldsBulk() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/fields/{key}",
|
||||
* summary="필드 설정 단건 수정",
|
||||
* description="특정 field_key에 대한 테넌트 설정을 수정합니다.",
|
||||
* tags={"Tenant.Fields"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(
|
||||
* name="key", in="path", required=true,
|
||||
* description="수정할 필드 키", @OA\Schema(type="string", example="position")
|
||||
* ),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/FieldSettingUpdateRequest")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/FieldEffective"))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=400, description="유효성 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="필드 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function fieldsUpdateOne() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/opt-groups",
|
||||
* summary="옵션 그룹 목록",
|
||||
* description="해당 테넌트의 옵션 그룹 목록을 페이징으로 반환합니다.",
|
||||
* tags={"Tenant.Option Groups"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
* @OA\Parameter(name="q", in="query", required=false, description="검색어(그룹키/이름)", @OA\Schema(type="string", example="position")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="object",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/OptionGroup")),
|
||||
* @OA\Property(property="total", type="integer", example=3)
|
||||
* ))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function optGroupsIndex() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/opt-groups",
|
||||
* summary="옵션 그룹 생성",
|
||||
* description="옵션 그룹을 생성합니다.",
|
||||
* tags={"Tenant.Option Groups"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionGroupCreateRequest")),
|
||||
* @OA\Response(response=200, description="생성 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optGroupsStore() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/opt-groups/{id}",
|
||||
* summary="옵션 그룹 단건 조회",
|
||||
* tags={"Tenant.Option Groups"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optGroupsShow() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/opt-groups/{id}",
|
||||
* summary="옵션 그룹 수정",
|
||||
* tags={"Tenant.Option Groups"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionGroupUpdateRequest")),
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optGroupsUpdate() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/opt-groups/{id}",
|
||||
* summary="옵션 그룹 삭제",
|
||||
* tags={"Tenant.Option Groups"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Response(response=200, description="삭제 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="object", nullable=true, example=null))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optGroupsDestroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/opt-groups/{gid}/values",
|
||||
* summary="옵션 값 목록",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Parameter(name="active_only", in="query", required=false, @OA\Schema(type="boolean", example=true)),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/OptionValue")))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesIndex() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/opt-groups/{gid}/values",
|
||||
* summary="옵션 값 생성",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueCreateRequest")),
|
||||
* @OA\Response(response=200, description="생성 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesStore() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/opt-groups/{gid}/values/{id}",
|
||||
* summary="옵션 값 단건 조회",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesShow() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/opt-groups/{gid}/values/{id}",
|
||||
* summary="옵션 값 수정",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueUpdateRequest")),
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesUpdate() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/opt-groups/{gid}/values/{id}",
|
||||
* summary="옵션 값 삭제",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)),
|
||||
* @OA\Response(response=200, description="삭제 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="object", nullable=true, example=null))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesDestroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/opt-groups/{gid}/values/reorder",
|
||||
* summary="옵션 값 정렬 순서 일괄 변경",
|
||||
* tags={"Tenant.Option Values"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueReorderRequest")),
|
||||
* @OA\Response(response=200, description="변경 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="object", example={"reordered":true}))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function optionValuesReorder() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/profiles",
|
||||
* summary="프로필 목록",
|
||||
* description="테넌트 내 회원 프로필 목록을 페이징으로 반환합니다.",
|
||||
* tags={"Tenant.Profiles"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||
* @OA\Parameter(name="q", in="query", required=false, description="검색어(표기명/사번 등)", @OA\Schema(type="string", example="A-001")),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ProfilePagination"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function profilesIndex() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/profiles/{userId}",
|
||||
* summary="프로필 단건 조회",
|
||||
* tags={"Tenant.Profiles"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="userId", in="path", required=true, @OA\Schema(type="integer", example=55)),
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function profilesShow() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/profiles/{userId}",
|
||||
* summary="프로필 수정(관리자)",
|
||||
* description="관리자 권한으로 해당 사용자의 프로필을 수정합니다. 테넌트에서 enabled된 필드만 반영됩니다.",
|
||||
* tags={"Tenant.Profiles"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Parameter(name="userId", in="path", required=true, @OA\Schema(type="integer", example=55)),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProfileUpdateRequest")),
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function profilesUpdate() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/profiles/me",
|
||||
* summary="내 프로필 조회",
|
||||
* tags={"Tenant.Profiles"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\Response(response=200, description="성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function profilesMe() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/profiles/me",
|
||||
* summary="내 프로필 수정",
|
||||
* tags={"Tenant.Profiles"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProfileUpdateRequest")),
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile"))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function profilesUpdateMe() {}
|
||||
}
|
||||
@@ -8,6 +8,11 @@
|
||||
<rapi-doc
|
||||
id="doc"
|
||||
spec-url="/docs?api-docs.json"
|
||||
|
||||
sort-tags="true"
|
||||
tag-sorter="alpha"
|
||||
sort-endpoints-by="path"
|
||||
|
||||
render-style="focused"
|
||||
theme="light"
|
||||
show-method-in-nav-bar="as-colored-block"
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
use App\Http\Controllers\Api\V1\RolePermissionController;
|
||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||
use App\Http\Controllers\Api\V1\DepartmentController;
|
||||
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
||||
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
||||
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
||||
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
||||
|
||||
// error test
|
||||
Route::get('/test-error', function () {
|
||||
@@ -175,5 +179,38 @@
|
||||
Route::delete('/{id}/permissions/{permission}', [DepartmentController::class, 'revokePermission'])->name('departments.permissions.revoke'); // 권한 제거(해당 메뉴 범위까지)
|
||||
});
|
||||
|
||||
|
||||
// 테넌트 필드 설정
|
||||
Route::prefix('fields')->group(function () {
|
||||
Route::get ('', [TenantFieldSettingController::class, 'index']); // 목록(효과값)
|
||||
Route::put ('/bulk', [TenantFieldSettingController::class, 'bulkUpsert']); // 대량 저장
|
||||
Route::patch ('/{key}', [TenantFieldSettingController::class, 'updateOne']); // 단건 수정
|
||||
});
|
||||
|
||||
// 옵션 그룹/값
|
||||
Route::prefix('opt-groups')->group(function () {
|
||||
Route::get ('', [TenantOptionGroupController::class, 'index']);
|
||||
Route::post ('', [TenantOptionGroupController::class, 'store']);
|
||||
Route::get ('/{id}', [TenantOptionGroupController::class, 'show']);
|
||||
Route::patch ('/{id}', [TenantOptionGroupController::class, 'update']);
|
||||
Route::delete('/{id}', [TenantOptionGroupController::class, 'destroy']);
|
||||
|
||||
Route::get ('/{gid}/values', [TenantOptionValueController::class, 'index']);
|
||||
Route::post ('/{gid}/values', [TenantOptionValueController::class, 'store']);
|
||||
Route::get ('/{gid}/values/{id}', [TenantOptionValueController::class, 'show']);
|
||||
Route::patch ('/{gid}/values/{id}', [TenantOptionValueController::class, 'update']);
|
||||
Route::delete ('/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy']);
|
||||
Route::patch ('/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder']); // [{id,sort_order}]
|
||||
});
|
||||
|
||||
// 회원 프로필(테넌트 기준)
|
||||
Route::prefix('profiles')->group(function () {
|
||||
Route::get ('', [TenantUserProfileController::class, 'index']); // 목록
|
||||
Route::get ('/{userId}', [TenantUserProfileController::class, 'show']); // 단건
|
||||
Route::patch ('/{userId}', [TenantUserProfileController::class, 'update']); // 수정(관리자)
|
||||
Route::get ('/me', [TenantUserProfileController::class, 'me']); // 내 프로필
|
||||
Route::patch ('/me', [TenantUserProfileController::class, 'updateMe']); // 내 정보 수정
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user