feat: Phase 6.2 팝업관리 API 구현
- popups 테이블 마이그레이션 생성 - Popup 모델 (BelongsToTenant, SoftDeletes) - PopupService CRUD 구현 - FormRequest 검증 (Store/Update) - PopupController 6개 엔드포인트 - Swagger 문서 (PopupApi.php) - PROJECT_DEVELOPMENT_POLICY.md 정책 준수
This commit is contained in:
@@ -1846,4 +1846,64 @@ ### 주요 작업
|
||||
- 계층 구조 및 필드 관리 테스트
|
||||
- Validation 로직 개선
|
||||
|
||||
---
|
||||
---
|
||||
## 2025-12-19 (목) - Phase 6.2 팝업관리 API 구현
|
||||
|
||||
### 주요 작업
|
||||
- Phase 6.2 팝업관리 기능 구현 완료
|
||||
- PROJECT_DEVELOPMENT_POLICY.md 정책 준수 (string 타입, options JSON 가변 컬럼)
|
||||
|
||||
### 추가된 파일
|
||||
|
||||
**마이그레이션:**
|
||||
- `database/migrations/2025_12_19_170001_create_popups_table.php`
|
||||
|
||||
**모델:**
|
||||
- `app/Models/Popups/Popup.php`
|
||||
- BelongsToTenant, SoftDeletes 적용
|
||||
- target_type: all(전사), department(부서)
|
||||
- status: active(사용), inactive(사용안함)
|
||||
- 스코프: active(), status(), targetType(), forUser()
|
||||
- 관계: department(), creator(), updater()
|
||||
|
||||
**서비스:**
|
||||
- `app/Services/PopupService.php`
|
||||
- index(): 관리자용 목록 (페이지네이션)
|
||||
- getActivePopups(): 사용자용 활성 팝업
|
||||
- show(): 상세 조회
|
||||
- store(): 등록
|
||||
- update(): 수정
|
||||
- destroy(): 삭제 (Soft Delete)
|
||||
|
||||
**FormRequest:**
|
||||
- `app/Http/Requests/V1/Popup/StorePopupRequest.php`
|
||||
- `app/Http/Requests/V1/Popup/UpdatePopupRequest.php`
|
||||
|
||||
**컨트롤러:**
|
||||
- `app/Http/Controllers/Api/V1/PopupController.php`
|
||||
|
||||
**Swagger:**
|
||||
- `app/Swagger/v1/PopupApi.php`
|
||||
|
||||
### 수정된 파일
|
||||
- `routes/api.php`: Popup 라우트 추가 (6개 엔드포인트)
|
||||
|
||||
### API 엔드포인트 (6개)
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | /api/v1/popups | 팝업 목록 (관리자용) |
|
||||
| POST | /api/v1/popups | 팝업 등록 |
|
||||
| GET | /api/v1/popups/active | 활성 팝업 (사용자용) |
|
||||
| GET | /api/v1/popups/{id} | 팝업 상세 |
|
||||
| PUT | /api/v1/popups/{id} | 팝업 수정 |
|
||||
| DELETE | /api/v1/popups/{id} | 팝업 삭제 |
|
||||
|
||||
### 정책 준수 사항
|
||||
- ✅ 기존 테이블 확인 후 신규 생성
|
||||
- ✅ string 타입 사용 (enum 대신)
|
||||
- ✅ options JSON 가변 컬럼
|
||||
- ✅ BelongsToTenant, SoftDeletes 적용
|
||||
- ✅ Service-First 아키텍처
|
||||
- ✅ FormRequest 검증
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-12-19 15:55:41
|
||||
> **자동 생성**: 2025-12-19 16:12:19
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -320,6 +320,13 @@ ### role_menu_permissions
|
||||
- **role()**: belongsTo → `roles`
|
||||
- **menu()**: belongsTo → `menus`
|
||||
|
||||
### popups
|
||||
**모델**: `App\Models\Popups\Popup`
|
||||
|
||||
- **department()**: belongsTo → `departments`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### prices
|
||||
**모델**: `App\Models\Products\Price`
|
||||
|
||||
|
||||
89
app/Http/Controllers/Api/V1/PopupController.php
Normal file
89
app/Http/Controllers/Api/V1/PopupController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Popup\StorePopupRequest;
|
||||
use App\Http\Requests\V1\Popup\UpdatePopupRequest;
|
||||
use App\Services\PopupService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PopupController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PopupService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 팝업 목록 (관리자용)
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'target_type',
|
||||
'status',
|
||||
'search',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$popups = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($popups, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 팝업 목록 (사용자용)
|
||||
*/
|
||||
public function active(Request $request)
|
||||
{
|
||||
$departmentId = $request->input('department_id');
|
||||
|
||||
$popups = $this->service->getActivePopups($departmentId);
|
||||
|
||||
return ApiResponse::success($popups, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 등록
|
||||
*/
|
||||
public function store(StorePopupRequest $request)
|
||||
{
|
||||
$popup = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($popup, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$popup = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($popup, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 수정
|
||||
*/
|
||||
public function update(int $id, UpdatePopupRequest $request)
|
||||
{
|
||||
$popup = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($popup, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
55
app/Http/Requests/V1/Popup/StorePopupRequest.php
Normal file
55
app/Http/Requests/V1/Popup/StorePopupRequest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Popup;
|
||||
|
||||
use App\Models\Popups\Popup;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StorePopupRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => ['sometimes', 'string', Rule::in(array_keys(Popup::TARGET_TYPES))],
|
||||
'target_id' => ['nullable', 'integer', 'exists:departments,id'],
|
||||
'title' => ['required', 'string', 'max:200'],
|
||||
'content' => ['required', 'string'],
|
||||
'status' => ['sometimes', 'string', Rule::in(array_keys(Popup::STATUSES))],
|
||||
'started_at' => ['nullable', 'date'],
|
||||
'ended_at' => ['nullable', 'date', 'after_or_equal:started_at'],
|
||||
'options' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'target_type.in' => __('validation.in', ['attribute' => '대상 유형']),
|
||||
'target_id.exists' => __('validation.exists', ['attribute' => '대상 부서']),
|
||||
'title.required' => __('validation.required', ['attribute' => '제목']),
|
||||
'title.max' => __('validation.max.string', ['attribute' => '제목', 'max' => 200]),
|
||||
'content.required' => __('validation.required', ['attribute' => '내용']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
'ended_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '시작일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Requests/V1/Popup/UpdatePopupRequest.php
Normal file
53
app/Http/Requests/V1/Popup/UpdatePopupRequest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Popup;
|
||||
|
||||
use App\Models\Popups\Popup;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdatePopupRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => ['sometimes', 'string', Rule::in(array_keys(Popup::TARGET_TYPES))],
|
||||
'target_id' => ['nullable', 'integer', 'exists:departments,id'],
|
||||
'title' => ['sometimes', 'string', 'max:200'],
|
||||
'content' => ['sometimes', 'string'],
|
||||
'status' => ['sometimes', 'string', Rule::in(array_keys(Popup::STATUSES))],
|
||||
'started_at' => ['nullable', 'date'],
|
||||
'ended_at' => ['nullable', 'date', 'after_or_equal:started_at'],
|
||||
'options' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'target_type.in' => __('validation.in', ['attribute' => '대상 유형']),
|
||||
'target_id.exists' => __('validation.exists', ['attribute' => '대상 부서']),
|
||||
'title.max' => __('validation.max.string', ['attribute' => '제목', 'max' => 200]),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
'ended_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '시작일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
189
app/Models/Popups/Popup.php
Normal file
189
app/Models/Popups/Popup.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Popups;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Popup extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'target_type',
|
||||
'target_id',
|
||||
'title',
|
||||
'content',
|
||||
'status',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'target_id' => 'integer',
|
||||
'options' => 'array',
|
||||
'started_at' => 'datetime',
|
||||
'ended_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 대상 유형 상수
|
||||
*/
|
||||
public const TARGET_ALL = 'all';
|
||||
|
||||
public const TARGET_DEPARTMENT = 'department';
|
||||
|
||||
/**
|
||||
* 대상 유형 목록
|
||||
*/
|
||||
public const TARGET_TYPES = [
|
||||
self::TARGET_ALL => '전사',
|
||||
self::TARGET_DEPARTMENT => '부서',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 상수
|
||||
*/
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
public const STATUS_INACTIVE = 'inactive';
|
||||
|
||||
/**
|
||||
* 상태 목록
|
||||
*/
|
||||
public const STATUSES = [
|
||||
self::STATUS_ACTIVE => '사용',
|
||||
self::STATUS_INACTIVE => '사용안함',
|
||||
];
|
||||
|
||||
/**
|
||||
* 부서 관계 (target_type='department' 일 때)
|
||||
*/
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'target_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자 관계
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 유형 라벨 속성
|
||||
*/
|
||||
public function getTargetTypeLabelAttribute(): string
|
||||
{
|
||||
return self::TARGET_TYPES[$this->target_type] ?? $this->target_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨 속성
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상명 속성 (전사 또는 부서명)
|
||||
*/
|
||||
public function getTargetNameAttribute(): string
|
||||
{
|
||||
if ($this->target_type === self::TARGET_ALL) {
|
||||
return '전사';
|
||||
}
|
||||
|
||||
return $this->department?->name ?? '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 스코프 (상태=active AND 기간 내)
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
$now = now();
|
||||
|
||||
return $query->where('status', self::STATUS_ACTIVE)
|
||||
->where(function ($q) use ($now) {
|
||||
$q->whereNull('started_at')
|
||||
->orWhere('started_at', '<=', $now);
|
||||
})
|
||||
->where(function ($q) use ($now) {
|
||||
$q->whereNull('ended_at')
|
||||
->orWhere('ended_at', '>=', $now);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 스코프
|
||||
*/
|
||||
public function scopeStatus(Builder $query, string $status): Builder
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 유형별 스코프
|
||||
*/
|
||||
public function scopeTargetType(Builder $query, string $targetType): Builder
|
||||
{
|
||||
return $query->where('target_type', $targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 대상 팝업 스코프 (전사 또는 사용자 부서)
|
||||
*/
|
||||
public function scopeForUser(Builder $query, ?int $departmentId = null): Builder
|
||||
{
|
||||
return $query->where(function ($q) use ($departmentId) {
|
||||
$q->where('target_type', self::TARGET_ALL);
|
||||
|
||||
if ($departmentId) {
|
||||
$q->orWhere(function ($q) use ($departmentId) {
|
||||
$q->where('target_type', self::TARGET_DEPARTMENT)
|
||||
->where('target_id', $departmentId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* options 헬퍼 메서드
|
||||
*/
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->options, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* options 설정 헬퍼 메서드
|
||||
*/
|
||||
public function setOption(string $key, mixed $value): void
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
data_set($options, $key, $value);
|
||||
$this->options = $options;
|
||||
}
|
||||
}
|
||||
163
app/Services/PopupService.php
Normal file
163
app/Services/PopupService.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Popups\Popup;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PopupService extends Service
|
||||
{
|
||||
/**
|
||||
* 팝업 목록 조회 (관리자용)
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Popup::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['creator:id,name', 'department:id,name']);
|
||||
|
||||
// 대상 유형 필터
|
||||
if (! empty($params['target_type'])) {
|
||||
$query->where('target_type', $params['target_type']);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('title', 'like', "%{$search}%")
|
||||
->orWhere('content', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 팝업 목록 조회 (사용자용 - 로그인 후 노출)
|
||||
*/
|
||||
public function getActivePopups(?int $departmentId = null): Collection
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Popup::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->active()
|
||||
->forUser($departmentId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 상세 조회
|
||||
*/
|
||||
public function show(int $id): Popup
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Popup::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['creator:id,name', 'department:id,name'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 등록
|
||||
*/
|
||||
public function store(array $data): Popup
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$popup = new Popup;
|
||||
$popup->tenant_id = $tenantId;
|
||||
$popup->target_type = $data['target_type'] ?? Popup::TARGET_ALL;
|
||||
$popup->target_id = $data['target_id'] ?? null;
|
||||
$popup->title = $data['title'];
|
||||
$popup->content = $data['content'];
|
||||
$popup->status = $data['status'] ?? Popup::STATUS_INACTIVE;
|
||||
$popup->started_at = $data['started_at'] ?? null;
|
||||
$popup->ended_at = $data['ended_at'] ?? null;
|
||||
$popup->options = $data['options'] ?? null;
|
||||
$popup->created_by = $userId;
|
||||
$popup->save();
|
||||
|
||||
return $popup->load(['creator:id,name', 'department:id,name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 수정
|
||||
*/
|
||||
public function update(int $id, array $data): Popup
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$popup = Popup::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (isset($data['target_type'])) {
|
||||
$popup->target_type = $data['target_type'];
|
||||
}
|
||||
if (array_key_exists('target_id', $data)) {
|
||||
$popup->target_id = $data['target_id'];
|
||||
}
|
||||
if (isset($data['title'])) {
|
||||
$popup->title = $data['title'];
|
||||
}
|
||||
if (isset($data['content'])) {
|
||||
$popup->content = $data['content'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$popup->status = $data['status'];
|
||||
}
|
||||
if (array_key_exists('started_at', $data)) {
|
||||
$popup->started_at = $data['started_at'];
|
||||
}
|
||||
if (array_key_exists('ended_at', $data)) {
|
||||
$popup->ended_at = $data['ended_at'];
|
||||
}
|
||||
if (array_key_exists('options', $data)) {
|
||||
$popup->options = $data['options'];
|
||||
}
|
||||
|
||||
$popup->updated_by = $userId;
|
||||
$popup->save();
|
||||
|
||||
return $popup->load(['creator:id,name', 'department:id,name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 삭제
|
||||
*/
|
||||
public function destroy(int $id): void
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$popup = Popup::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$popup->deleted_by = $userId;
|
||||
$popup->save();
|
||||
$popup->delete();
|
||||
}
|
||||
}
|
||||
246
app/Swagger/v1/PopupApi.php
Normal file
246
app/Swagger/v1/PopupApi.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Popup", description="팝업관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Popup",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="target_type", type="string", enum={"all", "department"}, example="all", description="대상 유형"),
|
||||
* @OA\Property(property="target_id", type="integer", nullable=true, example=null, description="대상 ID (부서 ID)"),
|
||||
* @OA\Property(property="title", type="string", example="공지사항 팝업", description="제목"),
|
||||
* @OA\Property(property="content", type="string", example="<p>팝업 내용입니다.</p>", description="내용 (HTML)"),
|
||||
* @OA\Property(property="status", type="string", enum={"active", "inactive"}, example="active", description="상태"),
|
||||
* @OA\Property(property="started_at", type="string", format="date-time", nullable=true, example="2025-01-01T00:00:00", description="노출 시작일"),
|
||||
* @OA\Property(property="ended_at", type="string", format="date-time", nullable=true, example="2025-12-31T23:59:59", description="노출 종료일"),
|
||||
* @OA\Property(property="options", type="object", nullable=true, description="확장 옵션"),
|
||||
* @OA\Property(property="created_by", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="updated_by", type="integer", nullable=true),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time"),
|
||||
* @OA\Property(
|
||||
* property="creator",
|
||||
* type="object",
|
||||
* nullable=true,
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string")
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="department",
|
||||
* type="object",
|
||||
* nullable=true,
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PopupPagination",
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/PaginationMeta"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(ref="#/components/schemas/Popup")
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PopupCreateRequest",
|
||||
* required={"title", "content"},
|
||||
*
|
||||
* @OA\Property(property="target_type", type="string", enum={"all", "department"}, example="all", description="대상 유형 (기본: all)"),
|
||||
* @OA\Property(property="target_id", type="integer", nullable=true, example=null, description="대상 부서 ID (target_type=department 시 필수)"),
|
||||
* @OA\Property(property="title", type="string", example="새 공지사항", maxLength=200, description="제목"),
|
||||
* @OA\Property(property="content", type="string", example="<p>팝업 내용</p>", description="내용 (HTML)"),
|
||||
* @OA\Property(property="status", type="string", enum={"active", "inactive"}, example="inactive", description="상태 (기본: inactive)"),
|
||||
* @OA\Property(property="started_at", type="string", format="date", nullable=true, example="2025-01-01", description="노출 시작일"),
|
||||
* @OA\Property(property="ended_at", type="string", format="date", nullable=true, example="2025-12-31", description="노출 종료일"),
|
||||
* @OA\Property(property="options", type="object", nullable=true, description="확장 옵션")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PopupUpdateRequest",
|
||||
*
|
||||
* @OA\Property(property="target_type", type="string", enum={"all", "department"}, example="department", description="대상 유형"),
|
||||
* @OA\Property(property="target_id", type="integer", nullable=true, example=1, description="대상 부서 ID"),
|
||||
* @OA\Property(property="title", type="string", example="수정된 공지사항", maxLength=200, description="제목"),
|
||||
* @OA\Property(property="content", type="string", example="<p>수정된 내용</p>", description="내용 (HTML)"),
|
||||
* @OA\Property(property="status", type="string", enum={"active", "inactive"}, example="active", description="상태"),
|
||||
* @OA\Property(property="started_at", type="string", format="date", nullable=true, example="2025-01-01", description="노출 시작일"),
|
||||
* @OA\Property(property="ended_at", type="string", format="date", nullable=true, example="2025-12-31", description="노출 종료일"),
|
||||
* @OA\Property(property="options", type="object", nullable=true, description="확장 옵션")
|
||||
* )
|
||||
*/
|
||||
class PopupApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/popups",
|
||||
* tags={"Popup"},
|
||||
* summary="팝업 목록 조회 (관리자용)",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="target_type", in="query", @OA\Schema(type="string", enum={"all", "department"}), description="대상 유형"),
|
||||
* @OA\Parameter(name="status", in="query", @OA\Schema(type="string", enum={"active", "inactive"}), description="상태"),
|
||||
* @OA\Parameter(name="search", in="query", @OA\Schema(type="string"), description="검색어 (제목, 내용)"),
|
||||
* @OA\Parameter(name="sort_by", in="query", @OA\Schema(type="string", default="created_at"), description="정렬 기준"),
|
||||
* @OA\Parameter(name="sort_dir", in="query", @OA\Schema(type="string", enum={"asc", "desc"}, default="desc"), description="정렬 방향"),
|
||||
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer", default=20), description="페이지당 항목 수"),
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", default=1), description="페이지 번호"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/PopupPagination")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/popups/active",
|
||||
* tags={"Popup"},
|
||||
* summary="활성 팝업 목록 조회 (사용자용 - 로그인 후 노출)",
|
||||
* description="현재 노출 중인 팝업 목록을 반환합니다. status=active이고 현재 시간이 started_at~ended_at 범위에 있는 팝업만 조회됩니다.",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="department_id", in="query", @OA\Schema(type="integer"), description="사용자의 부서 ID (해당 부서 대상 팝업 포함)"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(ref="#/components/schemas/Popup")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function active() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/popups",
|
||||
* tags={"Popup"},
|
||||
* summary="팝업 등록",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/PopupCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Popup")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/popups/{id}",
|
||||
* tags={"Popup"},
|
||||
* summary="팝업 상세 조회",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Popup")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/popups/{id}",
|
||||
* tags={"Popup"},
|
||||
* summary="팝업 수정",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/PopupUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Popup")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/popups/{id}",
|
||||
* tags={"Popup"},
|
||||
* summary="팝업 삭제",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="삭제 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('popups', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// 🔴 필수 컬럼 (조인/인덱싱)
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('target_type', 20)->default('all')->comment('대상 유형: all, department');
|
||||
$table->unsignedBigInteger('target_id')->nullable()->comment('대상 ID (부서 ID)');
|
||||
$table->string('title', 200)->comment('제목');
|
||||
$table->text('content')->comment('내용');
|
||||
$table->string('status', 20)->default('inactive')->index()->comment('상태: active, inactive');
|
||||
$table->dateTime('started_at')->nullable()->index()->comment('노출 시작일');
|
||||
$table->dateTime('ended_at')->nullable()->index()->comment('노출 종료일');
|
||||
|
||||
// 🟢 가변 컬럼 (JSON)
|
||||
$table->json('options')->nullable()->comment('확장 옵션');
|
||||
|
||||
// 감사 컬럼
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('작성자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index('target_type');
|
||||
$table->index(['tenant_id', 'status', 'started_at', 'ended_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('popups');
|
||||
}
|
||||
};
|
||||
@@ -59,6 +59,7 @@
|
||||
use App\Http\Controllers\Api\V1\PayrollController;
|
||||
use App\Http\Controllers\Api\V1\PermissionController;
|
||||
use App\Http\Controllers\Api\V1\PlanController;
|
||||
use App\Http\Controllers\Api\V1\PopupController;
|
||||
use App\Http\Controllers\Api\V1\PostController;
|
||||
use App\Http\Controllers\Api\V1\PricingController;
|
||||
use App\Http\Controllers\Api\V1\PurchaseController;
|
||||
@@ -533,6 +534,16 @@
|
||||
Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy');
|
||||
});
|
||||
|
||||
// Popup API (팝업관리)
|
||||
Route::prefix('popups')->group(function () {
|
||||
Route::get('', [PopupController::class, 'index'])->name('v1.popups.index');
|
||||
Route::post('', [PopupController::class, 'store'])->name('v1.popups.store');
|
||||
Route::get('/active', [PopupController::class, 'active'])->name('v1.popups.active');
|
||||
Route::get('/{id}', [PopupController::class, 'show'])->whereNumber('id')->name('v1.popups.show');
|
||||
Route::put('/{id}', [PopupController::class, 'update'])->whereNumber('id')->name('v1.popups.update');
|
||||
Route::delete('/{id}', [PopupController::class, 'destroy'])->whereNumber('id')->name('v1.popups.destroy');
|
||||
});
|
||||
|
||||
// Report API (보고서)
|
||||
Route::prefix('reports')->group(function () {
|
||||
Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');
|
||||
|
||||
Reference in New Issue
Block a user