Files
sam-api/app/Services/MenuService.php
hskwon a72a744612 feat: 글로벌 메뉴 분리 및 테넌트 메뉴 동기화 시스템 구현
- global_menus 테이블 분리를 위한 menus 컬럼 추가 (global_menu_id, is_customized)
- GlobalMenuController: 글로벌 메뉴 CRUD API
- GlobalMenuService: 글로벌 메뉴 비즈니스 로직
- MenuSyncService: 테넌트 메뉴 동기화 서비스
- MenuBootstrapService: 테넌트 초기 메뉴 생성 로직 개선
- MenuController: 메뉴 재동기화 엔드포인트 추가
2025-12-02 22:11:08 +09:00

312 lines
9.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Commons\Menu;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class MenuService
{
protected static function tenantId(): ?int
{
return app('tenant_id');
}
protected static function actorId(): ?int
{
$user = app('api_user'); // 컨테이너에 주입된 인증 사용자(객체 or 배열)
return is_object($user) ? ($user->id ?? null) : ($user['id'] ?? null);
}
/**
* 메뉴 목록 조회
*/
public static function index(array $params)
{
$tenantId = self::tenantId();
$q = Menu::query()->withShared($tenantId);
if (array_key_exists('parent_id', $params)) {
$q->where('parent_id', $params['parent_id']);
}
if (array_key_exists('is_active', $params)) {
$q->where('is_active', (int) $params['is_active']);
}
if (array_key_exists('hidden', $params)) {
$q->where('hidden', (int) $params['hidden']);
}
$q->orderBy('parent_id')->orderBy('sort_order');
// Builder 그대로 전달해야 쿼리로그/표준응답 형식 유지
return $q->get();
}
/**
* 메뉴 단건 조회
*/
public static function show(array $params)
{
$id = (int) ($params['id'] ?? 0);
$tenantId = self::tenantId();
if (! $id) {
return ['error' => 'id가 필요합니다.', 'code' => 400];
}
$res = Menu::withShared($tenantId)->find($id);
if (empty($res['data'])) {
return ['error' => 'Menu not found', 'code' => 404];
}
return $res;
}
/**
* 메뉴 생성
*/
public static function store(array $params)
{
$tenantId = self::tenantId();
$userId = self::actorId();
$v = Validator::make($params, [
'parent_id' => ['nullable', 'integer'],
'name' => ['required', 'string', 'max:100'],
'url' => ['nullable', 'string', 'max:255'],
'is_active' => ['nullable', 'boolean'],
'sort_order' => ['nullable', 'integer'],
'hidden' => ['nullable', 'boolean'],
'is_external' => ['nullable', 'boolean'],
'external_url' => ['nullable', 'string', 'max:255'],
'icon' => ['nullable', 'string', 'max:50'],
]);
if ($v->fails()) {
return ['error' => $v->errors()->first(), 'code' => 422];
}
$data = $v->validated();
$menu = new Menu;
$menu->tenant_id = $tenantId;
$menu->parent_id = $data['parent_id'] ?? null;
$menu->name = $data['name'];
$menu->url = $data['url'] ?? null;
$menu->is_active = (int) ($data['is_active'] ?? 1);
$menu->sort_order = (int) ($data['sort_order'] ?? 0);
$menu->hidden = (int) ($data['hidden'] ?? 0);
$menu->is_external = (int) ($data['is_external'] ?? 0);
$menu->external_url = $data['external_url'] ?? null;
$menu->icon = $data['icon'] ?? null;
$menu->created_by = $userId;
$menu->updated_by = $userId;
$menu->save();
// 생성 결과를 그대로 전달
return $menu->fresh();
}
/**
* 메뉴 수정
* - global_menu_id가 있는 테넌트 메뉴 수정 시 is_customized = true 자동 설정
*/
public static function update(array $params)
{
$id = (int) ($params['id'] ?? 0);
$tenantId = self::tenantId();
$userId = self::actorId();
if (! $id) {
return ['error' => 'id가 필요합니다.', 'code' => 400];
}
$v = Validator::make($params, [
'parent_id' => ['nullable', 'integer'],
'name' => ['nullable', 'string', 'max:100'],
'url' => ['nullable', 'string', 'max:255'],
'is_active' => ['nullable', 'boolean'],
'sort_order' => ['nullable', 'integer'],
'hidden' => ['nullable', 'boolean'],
'is_external' => ['nullable', 'boolean'],
'external_url' => ['nullable', 'string', 'max:255'],
'icon' => ['nullable', 'string', 'max:50'],
]);
if ($v->fails()) {
return ['error' => $v->errors()->first(), 'code' => 422];
}
$data = $v->validated();
$menu = Menu::withShared($tenantId)->where('id', $id)->first();
if (! $menu) {
return ['error' => 'Menu not found', 'code' => 404];
}
$update = Arr::only($data, [
'parent_id', 'name', 'url', 'is_active', 'sort_order', 'hidden', 'is_external', 'external_url', 'icon',
]);
$update = array_filter($update, fn ($v) => ! is_null($v));
if (empty($update)) {
return ['error' => '수정할 데이터가 없습니다.', 'code' => 400];
}
// 글로벌 메뉴에서 복제된 테넌트 메뉴 수정 시 커스터마이징 플래그 설정
if ($menu->global_menu_id && ! $menu->is_customized) {
$update['is_customized'] = true;
}
$update['updated_by'] = $userId;
$menu->fill($update)->save();
return $menu->fresh();
}
/**
* 메뉴 삭제(소프트)
*/
public static function destroy(array $params)
{
$id = (int) ($params['id'] ?? 0);
$tenantId = self::tenantId();
$userId = self::actorId();
if (! $id) {
return ['error' => 'id가 필요합니다.', 'code' => 400];
}
$menu = Menu::withShared($tenantId)->where('id', $id)->first();
if (! $menu) {
return ['error' => 'Menu not found', 'code' => 404];
}
$menu->deleted_by = $userId;
$menu->save();
$menu->delete();
return 'success';
}
/**
* 정렬 일괄 변경
* $params = [ ['id'=>10, 'sort_order'=>1], ... ]
*/
public static function reorder(array $params)
{
if (! is_array($params) || empty($params)) {
return ['error' => '유효한 정렬 목록이 필요합니다.', 'code' => 422];
}
$tenantId = self::tenantId();
DB::transaction(function () use ($params, $tenantId) {
foreach ($params as $it) {
if (! isset($it['id'], $it['sort_order'])) {
continue;
}
$menu = Menu::withShared($tenantId)->find((int) $it['id']);
if ($menu) {
$menu->sort_order = (int) $it['sort_order'];
$menu->save();
}
}
});
return 'success';
}
/**
* 상태 토글: is_active / hidden / is_external
*/
public static function toggle(array $params)
{
$id = (int) ($params['id'] ?? 0);
$tenantId = self::tenantId();
$userId = self::actorId();
if (! $id) {
return ['error' => 'id가 필요합니다.', 'code' => 400];
}
$payload = array_filter([
'is_active' => array_key_exists('is_active', $params) ? (int) $params['is_active'] : null,
'hidden' => array_key_exists('hidden', $params) ? (int) $params['hidden'] : null,
'is_external' => array_key_exists('is_external', $params) ? (int) $params['is_external'] : null,
], fn ($v) => ! is_null($v));
if (empty($payload)) {
return ['error' => '변경할 필드가 없습니다.', 'code' => 422];
}
$menu = Menu::withShared($tenantId)->find($id);
if (! $menu) {
return ['error' => 'Menu not found', 'code' => 404];
}
$payload['updated_by'] = $userId;
$menu->fill($payload)->save();
return $menu->fresh();
}
/**
* 삭제된 메뉴 복원
*/
public static function restore(array $params)
{
$id = (int) ($params['id'] ?? 0);
$tenantId = self::tenantId();
$userId = self::actorId();
if (! $id) {
return ['error' => 'id가 필요합니다.', 'code' => 400];
}
// 삭제된 메뉴 포함하여 조회
$menu = Menu::withTrashed()
->withoutGlobalScopes()
->where(function ($q) use ($tenantId) {
$q->whereNull('tenant_id')
->orWhere('tenant_id', $tenantId);
})
->where('id', $id)
->first();
if (! $menu) {
return ['error' => 'Menu not found', 'code' => 404];
}
if (! $menu->trashed()) {
return ['error' => '삭제되지 않은 메뉴입니다.', 'code' => 400];
}
$menu->restore();
$menu->deleted_by = null;
$menu->updated_by = $userId;
$menu->save();
return $menu->fresh();
}
/**
* 삭제된 메뉴 목록 조회
*/
public static function trashedList(array $params = [])
{
$tenantId = self::tenantId();
return Menu::onlyTrashed()
->withoutGlobalScopes()
->where(function ($q) use ($tenantId) {
$q->whereNull('tenant_id')
->orWhere('tenant_id', $tenantId);
})
->orderBy('deleted_at', 'desc')
->get();
}
}