fix : 테넌트 관리자 사용자 API 추가

- 사용자 목록
- 사용자 생성
- 사용자 단건 조회
- 사용자 수정
- 사용자 삭제(소프트 삭제)
- 활성/비활성 전환
- 삭제 복구
- 비밀번호 초기화

- 수정필요 : 역할부여, 역할 해재
This commit is contained in:
2025-08-15 16:32:11 +09:00
parent 9935bba84e
commit 06197a1366
7 changed files with 644 additions and 145 deletions

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Helpers\ApiResponse;
use App\Models\SiteAdmin;
class AdminApiController extends Controller
{
/* /**
* @OA\Post(
* path="/api/v1/admin/list",
* summary="관리자 리스트",
* tags={"Admin"},
* security={{"ApiKeyAuth":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"user_token"},
* @OA\Property(property="user_token", type="string", example="XXXXXXX")
* )
* ),
* @OA\Response(response=200, description="성공"),
* @OA\Response(response=401, description="실패")
* )
*/
public function list(Request $request)
{
return ApiResponse::handle(function () use ($request) {
$admins = new SiteAdmin;
return ApiResponse::response('get', $admins, $request->debug);
}, '관리자 목록 조회 성공', '관리자 목록 조회 실패');
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\AdminService;
use App\Helpers\ApiResponse;
class AdminController extends Controller
{
public function index(Request $request)
{
return ApiResponse::handle(function () use ($request) {
return AdminService::getTenants($request->all());
}, '테넌트 사용자 목록 조회');
}
public function store(Request $request)
{
return ApiResponse::handle(function () use ($request) {
return AdminService::store($request->all());
}, '테넌트 사용자 추가');
}
public function show($userNo)
{
return ApiResponse::handle(function () use ($userNo) {
return AdminService::show($userNo);
}, '테넌트 사용자 단건 조회');
}
public function update(Request $request, $userNo)
{
return ApiResponse::handle(function () use ($request, $userNo) {
return AdminService::update($request->all(), $userNo);
}, '테넌트 사용자 수정');
}
public function destroy($userNo)
{
return ApiResponse::handle(function () use ($userNo) {
return AdminService::destroy($userNo);
}, '테넌트 사용자 삭제(연결 삭제)');
}
public function restore($userNo)
{
return ApiResponse::handle(function () use ($userNo) {
return AdminService::restore($userNo);
}, '테넌트 사용자 삭제 복구');
}
public function toggle($userNo)
{
return ApiResponse::handle(function () use ($userNo) {
return AdminService::toggle($userNo);
}, '테넌트 사용자 활성/비활성');
}
public function attach(Request $request)
{
return ApiResponse::handle(function () use ($request) {
return AdminService::attach($request->all());
}, '테넌트 사용자 역할 부여');
}
public function detach(Request $request)
{
return ApiResponse::handle(function () use ($request) {
return AdminService::detach($request->all());
}, '테넌트 사용자 역할 해제');
}
public function reset(Request $request, $userNo)
{
return ApiResponse::handle(function () use ($request, $userNo) {
return AdminService::reset($request->all(), $userNo);
}, '테넌트 사용자 비밀번호 초기화');
}
}

View File

@@ -17,34 +17,21 @@ class User extends Authenticatable
use HasApiTokens, Notifiable, SoftDeletes, ModelTrait;
protected $fillable = [
'user_id',
'name',
'phone',
'email',
'phone',
'password',
'options',
'profile_photo_path',
];
protected $guarded = [
'id',
'user_id',
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
'email_verified_at',
'last_login_at',
'current_team_id',
'deleted_at',
'created_at',
'updated_at',
];
protected $casts = [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'options' => 'array',
'deleted_at' => 'datetime',
'password' => 'hashed', // ← 이걸 쓰면 자동 해싱
];
protected $hidden = [

View File

@@ -0,0 +1,469 @@
<?php
namespace App\Services;
use App\Helpers\ApiResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Models\Members\User;
use App\Models\Members\UserTenant;
use Spatie\Permission\Models\Role;
class AdminService
{
/**
* [GET] 테넌트 사용자 목록
* - 컨트롤러 index()에서 호출
* - 검색/정렬/페이징 최소 항목 포함
*/
public static function getTenants(array $params = [])
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
$page = isset($params['page']) ? (int)$params['page'] : 1;
$size = isset($params['size']) ? (int)$params['size'] : 10;
$keyword = $params['q'] ?? null;
$active = $params['is_active'] ?? null; // 0/1
$sortBy = $params['sort_by'] ?? 'users.id';
$sortDir = strtolower($params['sort_dir'] ?? 'desc') === 'asc' ? 'asc' : 'desc';
$q = UserTenant::query()
->with(['user:id,name,email,phone'])
->where('tenant_id', $tenantId);
if ($keyword) {
$q->whereHas('user', function($sub) use ($keyword) {
$sub->where(function($w) use ($keyword) {
$w->where('name', 'like', "%{$keyword}%")
->orWhere('email', 'like', "%{$keyword}%")
->orWhere('phone', 'like', "%{$keyword}%");
});
});
}
if ($active !== null && $active !== '') {
$q->where('is_active', (int)$active);
}
// 조인 정렬용
$q->leftJoin('users', 'users.id', '=', 'user_tenants.user_id')
->select(
'users.id',
'users.user_id',
'users.name',
'users.email',
'users.phone',
'user_tenants.is_active',
'user_tenants.joined_at',
'user_tenants.left_at',
'user_tenants.tenant_id'
);
$q->orderBy($sortBy, $sortDir);
$data = $q->paginate($size, ['*'], 'page', $page);
return ApiResponse::response('result', $data);
}
/**
* [POST] 테넌트 사용자 추가 (기존 사용자 연결)
* - 컨트롤러 store()에서 호출
* - 유저 등록 역할 부여
*/
public static function store(array $params = [])
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
// 신규 회원 생성 + 역할 부여 지원
$v = Validator::make($params, [
'user_id' => 'required|string|max:255|unique:users,user_id',
'name' => 'required|string|max:255',
'email' => 'required|email|max:100|unique:users,email',
'phone' => 'nullable|string|max:30',
'password' => 'required|string|min:8|max:64',
'roles' => 'nullable|array',
'roles.*' => 'string|max:100', // 각각의 역할 이름
]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$payload = $v->validated();
return DB::transaction(function () use ($payload, $tenantId) {
// 신규 사용자 생성
$user = User::create([
'user_id' => $payload['user_id'],
'name' => $payload['name'],
'email' => $payload['email'],
'phone' => $payload['phone'] ?? null,
'password' => $payload['password'], // 캐스트가 알아서 해싱
]);
// 현재 테넌트에 활성 연결
UserTenant::create([
'user_id' => $user->id,
'tenant_id' => $tenantId,
'is_active' => 1,
'is_default' => 0,
'joined_at' => now(),
]);
// 역할 부여 (Spatie Permission teams 모드 가정)
if (!empty($payload['roles']) && method_exists($user, 'assignRole')) {
$previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null;
app()->instance('permission.team_id', $tenantId);
try {
foreach ($payload['roles'] as $roleName) {
$user->assignRole($roleName);
}
} finally {
app()->instance('permission.team_id', $previousTeam);
}
}
return ApiResponse::response('result', [
'user' => $user->only(['id','user_id','name','email','phone']),
]);
});
}
/**
* [GET] 테넌트 사용자 단건 조회
* - 컨트롤러 show()에서 호출
*/
public static function show(int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
$user = User::whereHas('userTenants')->find($userNo);
if (!$user) {
return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404);
}
return ApiResponse::response('result', $user);
}
/**
* [PUT/PATCH] 테넌트 사용자 정보 수정
* - 회원 기본정보(user_id, name, email, phone, password) 변경
* - 역할(roles) 변경 및 삭제 처리
*/
public static function update(array $params = [], int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
// 1) 유저 존재/테넌트 소속 확인
$user = User::find($userNo);
if (!$user) {
return ApiResponse::error('해당 회원을 찾을 수 없습니다.', 404);
}
$linked = UserTenant::where('tenant_id', $tenantId)
->where('user_id', $userNo)
->exists();
if (!$linked) {
return ApiResponse::error('이 테넌트에 소속된 회원이 아닙니다.', 403);
}
// 2) 프로필 + roles만 수정
$v = Validator::make($params, [
'user_id' => ['nullable','string','max:255', Rule::unique('users','user_id')->ignore($userNo)],
'name' => 'nullable|string|max:255',
'email' => ['nullable','email','max:100', Rule::unique('users','email')->ignore($userNo)],
'phone' => 'nullable|string|max:30',
'password' => 'nullable|string|min:8|max:64',
'roles' => 'nullable|array',
'roles.*' => 'string|max:100',
]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$payload = $v->validated();
// 아무 필드도 없으면 방어
$updatableKeys = ['user_id','name','email','phone','password'];
$hasProfileInput = (bool) array_intersect(array_keys($payload), $updatableKeys);
$hasRolesInput = array_key_exists('roles', $payload);
if (!$hasProfileInput && !$hasRolesInput) {
return ApiResponse::error('수정할 항목이 없습니다.', 422);
}
return DB::transaction(function () use ($user, $payload, $tenantId, $updatableKeys) {
// 3) 프로필 업데이트 (제공된 키만 반영)
$updateData = [];
foreach ($updatableKeys as $k) {
if (array_key_exists($k, $payload)) {
$updateData[$k] = $payload[$k];
}
}
// 비밀번호 처리
if (array_key_exists('password', $updateData)) {
if ($updateData['password'] === null || $updateData['password'] === '') {
unset($updateData['password']); // 빈 값 들어오면 무시
}
}
if (!empty($updateData)) {
$user->fill($updateData);
$user->save();
}
// 4) 역할 수정 (teams 모드: 테넌트 컨텍스트로 sync)
if (array_key_exists('roles', $payload) && method_exists($user, 'syncRoles')) {
$previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null;
app()->instance('permission.team_id', $tenantId);
try {
// roles 키가 있으면 그 값으로 덮어쓰기 (빈 배열이면 모두 제거)
$roles = $payload['roles'] ?? [];
$user->syncRoles($roles);
} finally {
app()->instance('permission.team_id', $previousTeam);
}
}
return ApiResponse::response('result', [
'user' => $user->only(['id','user_id','name','email','phone']),
'roles' => method_exists($user, 'getRoleNames') ? $user->getRoleNames() : [],
]);
});
}
/**
* [DELETE] 테넌트 사용자 삭제(연결 해제)
* - soft delete + left_at 기록
*/
public static function destroy(int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
$ut = UserTenant::where('user_id',$userNo)
->where('tenant_id', $tenantId)
->first();
if (!$ut) {
return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404);
}
$ut->left_at = now();
$ut->save();
$ut->delete(); // SoftDeletes 가정
return ApiResponse::response('success');
}
/**
* [POST] 삭제 복구
*/
public static function restore(int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
$ut = UserTenant::withTrashed()
->where('tenant_id', $tenantId)
->where('user_id', $userNo)
->first();
if (!$ut) {
return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404);
}
if ($ut->trashed()) {
$ut->restore();
$ut->left_at = null;
$ut->save();
}
return ApiResponse::response('success');
}
/**
* [PATCH] 활성/비활성 토글
*/
public static function toggle(int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
$ut = UserTenant::where('tenant_id', $tenantId)
->where('user_id', $userNo)
->first();
if (!$ut) {
return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404);
}
$ut->is_active = $ut->is_active ? 0 : 1;
$ut->save();
return ApiResponse::response('result',['is_active' => $ut->is_active]);
}
/**
* [POST] 역할 부여 (Spatie Permission - teams 사용 가정)
* - params: user_id, role_name
*/
public static function attach(array $params = [])
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
$v = Validator::make($params, [
'user_id' => 'required|integer|exists:users,id',
'role_name' => 'required|string|max:100',
]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$user = User::find($params['user_id']);
if (!method_exists($user, 'assignRole')) {
// Spatie 미사용 환경 방어
return ApiResponse::error('역할 시스템이 활성화되어 있지 않습니다.', 501);
}
// teams(tenant) 스코프
$previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null;
app()->instance('permission.team_id', $tenantId);
try {
$user->assignRole($params['role_name']);
} finally {
// 원복
app()->instance('permission.team_id', $previousTeam);
}
return ApiResponse::response('success');
}
/**
* [POST] 역할 해제 (Spatie Permission - teams 사용 가정)
* - params: user_id, role_name
*/
public static function detach(array $params = [])
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
$v = Validator::make($params, [
'user_id' => 'required|integer|exists:users,id',
'role_name' => 'required|string|max:100',
]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$user = User::find($params['user_id']);
if (!method_exists($user, 'removeRole')) {
return ApiResponse::error('역할 시스템이 활성화되어 있지 않습니다.', 501);
}
$previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null;
app()->instance('permission.team_id', $tenantId);
try {
$user->removeRole($params['role_name']);
} finally {
app()->instance('permission.team_id', $previousTeam);
}
return ApiResponse::response('success');
}
/**
* [POST] 테넌트 사용자 비밀번호 초기화
* - (보안) 관리자 권한 확인은 미들웨어/가드에서 처리 가정
* - 새 임시 비밀번호를 설정(응답으로 직접 노출 X 권장)
* - 여기서는 옵션에 따라 노출/미노출 선택 가능하도록 구현
*/
public static function reset(array $params = [],int $userNo)
{
$tenantId = app('tenant_id');
if (!$tenantId) {
return ApiResponse::error('활성 테넌트가 없습니다.', 400);
}
if (!$userNo) {
return ApiResponse::error('회원 정보가 없습니다.', 422);
}
$v = Validator::make($params, [
'new_password' => 'nullable|string|min:8|max:64',
'return_password' => 'nullable|in:0,1', // 1이면 응답에 임시 비번 포함(개발용)
]);
if ($v->fails()) {
return ApiResponse::error($v->errors()->first(), 422);
}
$payload = $v->validated();
$user = User::find($userNo);
if (!$user) {
return ApiResponse::error('유저를 찾을 수 없습니다.', 404);
}
$new = $payload['new_password'] ?? Str::random(12);
$user->password = $new;
$user->save();
// (선택) 기존 토큰 무효화
// if (method_exists($user, 'tokens')) { $user->tokens()->delete(); }
$resp = ['status' => 'ok'];
if (!empty($payload['return_password'])) {
// 운영에선 반환하지 말고 메일/문자 발송을 권장
$resp['temp_password'] = $new;
}
return ApiResponse::response('result', $resp);
}
}

View File

@@ -257,6 +257,14 @@ public static function storeTenants(array $params = [])
$tenant = Tenant::create($payload);
// 기존 기본값(is_default=1) 해제
$apiUser = app('api_user');
UserTenant::withoutGlobalScopes()
->where('user_id', $apiUser)
->where('is_default', 1)
->update(['is_default' => 0]);
// 성성된 테넌트를 나의 테넌트로 셋팅
$apiUser = app('api_user');
UserTenant::create([

View File

@@ -17,7 +17,7 @@ class AdminApi
* @OA\Parameter(name="q", in="query", description="이름/이메일 검색어", @OA\Schema(type="string")),
* @OA\Parameter(name="tenant_id", in="query", description="특정 테넌트로 필터", @OA\Schema(type="integer", example=1)),
* @OA\Parameter(name="role", in="query", description="역할 코드", @OA\Schema(type="string", example="manager")),
* @OA\Parameter(name="is_active", in="query", description="활성여부", @OA\Schema(type="boolean", example=true)),
* @OA\Parameter(name="is_active", in="query", description="활성여부", @OA\Schema(type="boolean", example=1)),
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
* @OA\Response(
@@ -57,7 +57,7 @@ public function index() {}
* @OA\JsonContent(
* type="object",
* required={"name","email","password"},
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="user_id", type="string", example="test001"),
* @OA\Property(property="name", type="string", example="김관리"),
* @OA\Property(property="email", type="string", example="admin@kdcorp.co.kr"),
* @OA\Property(property="password", type="string", example="Init!2345"),
@@ -124,7 +124,7 @@ public function show() {}
* type="object",
* @OA\Property(property="name", type="string", example="김관리"),
* @OA\Property(property="phone", type="string", example="010-3333-4444"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="is_active", type="integer", example=1),
* @OA\Property(property="roles", type="array", @OA\Items(type="string"), example={"manager","staff"})
* )
* ),
@@ -144,25 +144,29 @@ public function update() {}
* path="/api/v1/admin/users/{id}/status",
* tags={"Admin-Users"},
* summary="활성/비활성 전환",
* description="is_active 토글",
* description="지정된 사용자의 is_active 상태를 변경합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(required=true,
* @OA\JsonContent(type="object",
* required={"is_active"},
* @OA\Property(property="is_active", type="boolean", example=false)
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="사용자 고유 ID",
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="변경 성공",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="테넌트 사용자 활성/비활성 성공"),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="is_active", type="integer", example=1)
* )
* )
* ),
*
* @OA\Response(
* response=204,
* description="변경 성공(콘텐츠 없음)",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="변경 성공"),
* @OA\Property(property="data", type="object", nullable=true, example=null)
* )
* ),
* @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")),
@@ -187,7 +191,7 @@ public function toggleStatus() {}
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="변경 성공"),
* @OA\Property(property="data", type="object", nullable=true, example=null)
* @OA\Property(property="data", type="string", example="Success")
* )
* ),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
@@ -213,7 +217,7 @@ public function destroy() {}
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="변경 성공"),
* @OA\Property(property="data", type="object", nullable=true, example=null)
* @OA\Property(property="data", type="string", example="Success")
* )
* ),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
@@ -293,16 +297,40 @@ public function detachRole() {}
* path="/api/v1/admin/users/{id}/reset-password",
* tags={"Admin-Users"},
* summary="비밀번호 초기화",
* description="임시 비밀번호 발급(또는 링크 전송)",
* description="지정된 사용자의 비밀번호를 새 임시 비밀번호로 초기화합니다.
* - 관리자 권한 확인은 미들웨어/가드에서 처리됩니다.
* - 기본적으로 응답에 비밀번호를 노출하지 않으며, return_password=1일 때만 임시 비밀번호를 반환합니다(운영 환경에서는 노출 비권장).",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="사용자 고유 ID",
* @OA\Schema(type="integer")
* ),
* @OA\RequestBody(
* required=false,
* @OA\JsonContent(type="object",
* @OA\Property(property="temp_password", type="string", example="Temp!1234", description="미지정 시 서버에서 생성")
* @OA\JsonContent(
* type="object",
* @OA\Property(property="new_password", type="string", minLength=8, maxLength=64, example="Temp!1234", description="지정 시 해당 값으로 비밀번호 초기화, 미지정 시 서버에서 랜덤 생성"),
* @OA\Property(property="return_password", type="integer", enum={0,1}, example=0, description="1이면 응답에 임시 비밀번호 포함(개발/테스트용)")
* )
* ),
* @OA\Response(
* response=200,
* description="초기화 성공",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="테넌트 사용자 비밀번호 초기화 성공"),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="status", type="string", example="ok"),
* @OA\Property(property="temp_password", type="string", example="Temp!1234", nullable=true, description="return_password=1일 때만 포함")
* )
* )
* ),
* @OA\Response(response=200, description="초기화 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
* @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")),

View File

@@ -12,6 +12,7 @@
use App\Http\Controllers\Api\V1\BomController;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\TenantController;
use App\Http\Controllers\Api\V1\AdminController;
// error test
Route::get('/test-error', function () {
@@ -48,6 +49,32 @@
});
// Tenant Admin API
Route::prefix('admin')->group(function () {
// 목록/생성
Route::get('users', [AdminController::class, 'index'])->name('v1.admin.users.index'); // 테넌트 사용자 목록 조회
Route::post('users', [AdminController::class, 'store'])->name('v1.admin.users.store'); // 테넌트 사용자 생성
// 단건
Route::get('users/{id}', [AdminController::class, 'show'])->name('v1.admin.users.show'); // 테넌트 사용자 단건 조회
Route::put('users/{id}', [AdminController::class, 'update'])->name('v1.admin.users.update'); // 테넌트 사용자 수정
// 소프트 삭제 복구
Route::delete('users/{id}', [AdminController::class, 'destroy'])->name('v1.admin.users.destroy'); // 테넌트 사용자 삭제(연결 삭제)
Route::post('users/{id}/restore', [AdminController::class, 'restore'])->name('v1.admin.users.restore'); // 테넌트 사용자 삭제 복구
// 상태 토글
Route::patch('users/{id}/status', [AdminController::class, 'toggle'])->name('v1.admin.users.status.toggle'); // 테넌트 사용자 활성/비활성
// 역할 부여/해제
Route::post('users/{id}/roles', [AdminController::class, 'attach'])->name('v1.admin.users.roles.attach'); // 테넌트 사용자 역할 부여
Route::delete('users/{id}/roles/{role}', [AdminController::class, 'detach'])->name('v1.admin.users.roles.detach'); // 테넌트 사용자 역할 해제
// 비밀번호 초기화
Route::post('users/{id}/reset-password', [AdminController::class, 'reset'])->name('v1.admin.users.password.reset'); // 테넌트 사용자 비밀번호 초기화
});
// Member API
Route::prefix('users')->group(function () {
Route::get('index', [UserController::class, 'index'])->name('v1.users.index'); // 회원 목록 조회
@@ -91,51 +118,6 @@
});
// ─────────────────────────────────────────────────────────────
// 공통 미들웨어 메모:
// - 'apikey' : X-API-KEY 검사 미들웨어 (커스텀)
// - 'auth:sanctum' : Bearer 토큰(Sanctum) 인증
// 필요 시 app/Http/Kernel.php 의 $routeMiddleware 에 별칭 등록
// ─────────────────────────────────────────────────────────────
/*
|--------------------------------------------------------------------------
| V1 - User 영역 (본인 계정)
|--------------------------------------------------------------------------
| Swagger: UserApi.php
| - POST /api/v1/auth/login
| - POST /api/v1/auth/logout
| - GET /api/v1/users/me
| - PUT /api/v1/users/me
| - PUT /api/v1/users/me/password
| - GET /api/v1/users/me/tenants
| - PATCH /api/v1/users/me/tenants/switch
*/
Route::prefix('v1_DEV')
->middleware(['apikey']) // 모든 엔드포인트는 X-API-KEY 필요
->group(function () {
// Auth (User)
Route::prefix('auth')->group(function () {
Route::post('login', [\App\Http\Controllers\Api\V1\AuthController::class, 'login'])
->name('v1.auth.login'); // Bearer 불필요(로그인)
Route::post('logout', [\App\Http\Controllers\Api\V1\AuthController::class, 'logout'])
->middleware('auth:sanctum')
->name('v1.auth.logout');
});
// Users (me)
Route::prefix('users')->middleware('auth:sanctum')->group(function () {
Route::get('me', [\App\Http\Controllers\Api\V1\User\MeController::class, 'show'])->name('v1.users.me.show');
Route::put('me', [\App\Http\Controllers\Api\V1\User\MeController::class, 'update'])->name('v1.users.me.update');
Route::put('me/password', [\App\Http\Controllers\Api\V1\User\MeController::class, 'changePassword'])->name('v1.users.me.password');
Route::get('me/tenants', [\App\Http\Controllers\Api\V1\User\TenantController::class, 'index'])->name('v1.users.me.tenants.index');
Route::patch('me/tenants/switch', [\App\Http\Controllers\Api\V1\User\TenantController::class, 'switch'])->name('v1.users.me.tenants.switch');
});
});
/*
|--------------------------------------------------------------------------
@@ -157,25 +139,5 @@
->middleware(['apikey', 'auth:sanctum', 'can:admin']) // 예: 'can:admin' 또는 커스텀 'is_admin'
->group(function () {
// 목록/생성
Route::get('users', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'index'])->name('v1.admin.users.index');
Route::post('users', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'store'])->name('v1.admin.users.store');
// 단건
Route::get('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'show'])->name('v1.admin.users.show');
Route::put('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'update'])->name('v1.admin.users.update');
Route::delete('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'destroy'])->name('v1.admin.users.destroy');
// 상태 토글
Route::patch('users/{id}/status', [\App\Http\Controllers\Api\V1\Admin\UserStatusController::class, 'toggle'])->name('v1.admin.users.status.toggle');
// 소프트 삭제 복구
Route::post('users/{id}/restore', [\App\Http\Controllers\Api\V1\Admin\UserRestoreController::class, 'restore'])->name('v1.admin.users.restore');
// 역할 부여/해제
Route::post('users/{id}/roles', [\App\Http\Controllers\Api\V1\Admin\UserRoleController::class, 'attach'])->name('v1.admin.users.roles.attach');
Route::delete('users/{id}/roles/{role}', [\App\Http\Controllers\Api\V1\Admin\UserRoleController::class, 'detach'])->name('v1.admin.users.roles.detach');
// 비밀번호 초기화
Route::post('users/{id}/reset-password', [\App\Http\Controllers\Api\V1\Admin\UserPasswordController::class, 'reset'])->name('v1.admin.users.password.reset');
});