feat: 2.3 카드/계좌 관리 API 구현

- cards, bank_accounts 테이블 마이그레이션
- Card, BankAccount 모델 (카드번호 암호화)
- CardService, BankAccountService
- CardController, BankAccountController + FormRequest 4개
- API 엔드포인트 15개 (카드 7개, 계좌 8개)
- Swagger 문서 (CardApi.php, BankAccountApi.php)
This commit is contained in:
2025-12-17 21:02:20 +09:00
parent a1980adb20
commit e1b0c99d5d
15 changed files with 1916 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\BankAccount\StoreBankAccountRequest;
use App\Http\Requests\V1\BankAccount\UpdateBankAccountRequest;
use App\Http\Responses\ApiResponse;
use App\Services\BankAccountService;
use Illuminate\Http\Request;
class BankAccountController extends Controller
{
public function __construct(
private readonly BankAccountService $service
) {}
/**
* 계좌 목록
*/
public function index(Request $request)
{
$params = $request->only([
'search',
'status',
'assigned_user_id',
'is_primary',
'sort_by',
'sort_dir',
'per_page',
'page',
]);
$accounts = $this->service->index($params);
return ApiResponse::handle(__('message.fetched'), $accounts);
}
/**
* 계좌 등록
*/
public function store(StoreBankAccountRequest $request)
{
$account = $this->service->store($request->validated());
return ApiResponse::handle(__('message.created'), $account, 201);
}
/**
* 계좌 상세
*/
public function show(int $id)
{
$account = $this->service->show($id);
return ApiResponse::handle(__('message.fetched'), $account);
}
/**
* 계좌 수정
*/
public function update(int $id, UpdateBankAccountRequest $request)
{
$account = $this->service->update($id, $request->validated());
return ApiResponse::handle(__('message.updated'), $account);
}
/**
* 계좌 삭제
*/
public function destroy(int $id)
{
$this->service->destroy($id);
return ApiResponse::handle(__('message.deleted'));
}
/**
* 계좌 상태 토글 (사용/정지)
*/
public function toggle(int $id)
{
$account = $this->service->toggleStatus($id);
return ApiResponse::handle(__('message.updated'), $account);
}
/**
* 대표계좌 설정
*/
public function setPrimary(int $id)
{
$account = $this->service->setPrimary($id);
return ApiResponse::handle(__('message.updated'), $account);
}
/**
* 활성 계좌 목록 (셀렉트박스용)
*/
public function active()
{
$accounts = $this->service->getActiveAccounts();
return ApiResponse::handle(__('message.fetched'), $accounts);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Card\StoreCardRequest;
use App\Http\Requests\V1\Card\UpdateCardRequest;
use App\Http\Responses\ApiResponse;
use App\Services\CardService;
use Illuminate\Http\Request;
class CardController extends Controller
{
public function __construct(
private readonly CardService $service
) {}
/**
* 카드 목록
*/
public function index(Request $request)
{
$params = $request->only([
'search',
'status',
'assigned_user_id',
'sort_by',
'sort_dir',
'per_page',
'page',
]);
$cards = $this->service->index($params);
return ApiResponse::handle(__('message.fetched'), $cards);
}
/**
* 카드 등록
*/
public function store(StoreCardRequest $request)
{
$card = $this->service->store($request->validated());
return ApiResponse::handle(__('message.created'), $card, 201);
}
/**
* 카드 상세
*/
public function show(int $id)
{
$card = $this->service->show($id);
return ApiResponse::handle(__('message.fetched'), $card);
}
/**
* 카드 수정
*/
public function update(int $id, UpdateCardRequest $request)
{
$card = $this->service->update($id, $request->validated());
return ApiResponse::handle(__('message.updated'), $card);
}
/**
* 카드 삭제
*/
public function destroy(int $id)
{
$this->service->destroy($id);
return ApiResponse::handle(__('message.deleted'));
}
/**
* 카드 상태 토글 (사용/정지)
*/
public function toggle(int $id)
{
$card = $this->service->toggleStatus($id);
return ApiResponse::handle(__('message.updated'), $card);
}
/**
* 활성 카드 목록 (셀렉트박스용)
*/
public function active()
{
$cards = $this->service->getActiveCards();
return ApiResponse::handle(__('message.fetched'), $cards);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Requests\V1\BankAccount;
use Illuminate\Foundation\Http\FormRequest;
class StoreBankAccountRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'bank_code' => ['required', 'string', 'max:10'],
'bank_name' => ['required', 'string', 'max:50'],
'account_number' => ['required', 'string', 'max:30', 'regex:/^[\d-]+$/'],
'account_holder' => ['required', 'string', 'max:50'],
'account_name' => ['required', 'string', 'max:100'],
'status' => ['nullable', 'string', 'in:active,inactive'],
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
'is_primary' => ['nullable', 'boolean'],
];
}
public function messages(): array
{
return [
'bank_code.required' => __('validation.required', ['attribute' => __('validation.attributes.bank_code')]),
'bank_name.required' => __('validation.required', ['attribute' => __('validation.attributes.bank_name')]),
'account_number.required' => __('validation.required', ['attribute' => __('validation.attributes.account_number')]),
'account_number.regex' => __('validation.account_number_format'),
'account_holder.required' => __('validation.required', ['attribute' => __('validation.attributes.account_holder')]),
'account_name.required' => __('validation.required', ['attribute' => __('validation.attributes.account_name')]),
];
}
public function attributes(): array
{
return [
'bank_code' => __('validation.attributes.bank_code'),
'bank_name' => __('validation.attributes.bank_name'),
'account_number' => __('validation.attributes.account_number'),
'account_holder' => __('validation.attributes.account_holder'),
'account_name' => __('validation.attributes.account_name'),
'status' => __('validation.attributes.status'),
'assigned_user_id' => __('validation.attributes.assigned_user_id'),
'is_primary' => __('validation.attributes.is_primary'),
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests\V1\BankAccount;
use Illuminate\Foundation\Http\FormRequest;
class UpdateBankAccountRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'bank_code' => ['sometimes', 'string', 'max:10'],
'bank_name' => ['sometimes', 'string', 'max:50'],
'account_number' => ['sometimes', 'string', 'max:30', 'regex:/^[\d-]+$/'],
'account_holder' => ['sometimes', 'string', 'max:50'],
'account_name' => ['sometimes', 'string', 'max:100'],
'status' => ['sometimes', 'string', 'in:active,inactive'],
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
];
}
public function messages(): array
{
return [
'account_number.regex' => __('validation.account_number_format'),
];
}
public function attributes(): array
{
return [
'bank_code' => __('validation.attributes.bank_code'),
'bank_name' => __('validation.attributes.bank_name'),
'account_number' => __('validation.attributes.account_number'),
'account_holder' => __('validation.attributes.account_holder'),
'account_name' => __('validation.attributes.account_name'),
'status' => __('validation.attributes.status'),
'assigned_user_id' => __('validation.attributes.assigned_user_id'),
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Requests\V1\Card;
use Illuminate\Foundation\Http\FormRequest;
class StoreCardRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'card_company' => ['required', 'string', 'max:50'],
'card_number' => ['required', 'string', 'regex:/^\d{13,19}$/'],
'expiry_date' => ['required', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'],
'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'],
'card_name' => ['required', 'string', 'max:100'],
'status' => ['nullable', 'string', 'in:active,inactive'],
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
];
}
public function messages(): array
{
return [
'card_company.required' => __('validation.required', ['attribute' => __('validation.attributes.card_company')]),
'card_number.required' => __('validation.required', ['attribute' => __('validation.attributes.card_number')]),
'card_number.regex' => __('validation.card_number_format'),
'expiry_date.required' => __('validation.required', ['attribute' => __('validation.attributes.expiry_date')]),
'expiry_date.regex' => __('validation.expiry_date_format'),
'card_password.size' => __('validation.card_password_format'),
'card_password.regex' => __('validation.card_password_format'),
'card_name.required' => __('validation.required', ['attribute' => __('validation.attributes.card_name')]),
];
}
public function attributes(): array
{
return [
'card_company' => __('validation.attributes.card_company'),
'card_number' => __('validation.attributes.card_number'),
'expiry_date' => __('validation.attributes.expiry_date'),
'card_password' => __('validation.attributes.card_password'),
'card_name' => __('validation.attributes.card_name'),
'status' => __('validation.attributes.status'),
'assigned_user_id' => __('validation.attributes.assigned_user_id'),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests\V1\Card;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCardRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'card_company' => ['sometimes', 'string', 'max:50'],
'card_number' => ['sometimes', 'string', 'regex:/^\d{13,19}$/'],
'expiry_date' => ['sometimes', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'],
'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'],
'card_name' => ['sometimes', 'string', 'max:100'],
'status' => ['sometimes', 'string', 'in:active,inactive'],
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
];
}
public function messages(): array
{
return [
'card_number.regex' => __('validation.card_number_format'),
'expiry_date.regex' => __('validation.expiry_date_format'),
'card_password.size' => __('validation.card_password_format'),
'card_password.regex' => __('validation.card_password_format'),
];
}
public function attributes(): array
{
return [
'card_company' => __('validation.attributes.card_company'),
'card_number' => __('validation.attributes.card_number'),
'expiry_date' => __('validation.attributes.expiry_date'),
'card_password' => __('validation.attributes.card_password'),
'card_name' => __('validation.attributes.card_name'),
'status' => __('validation.attributes.status'),
'assigned_user_id' => __('validation.attributes.assigned_user_id'),
];
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Models\Tenants;
use App\Models\Members\User;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 은행 계좌 모델
*
* @property int $id
* @property int $tenant_id
* @property string $bank_code
* @property string $bank_name
* @property string $account_number
* @property string $account_holder
* @property string $account_name
* @property string $status
* @property int|null $assigned_user_id
* @property bool $is_primary
* @property int|null $created_by
* @property int|null $updated_by
* @property int|null $deleted_by
*/
class BankAccount extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $table = 'bank_accounts';
protected $fillable = [
'tenant_id',
'bank_code',
'bank_name',
'account_number',
'account_holder',
'account_name',
'status',
'assigned_user_id',
'is_primary',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_primary' => 'boolean',
];
protected $attributes = [
'status' => 'active',
'is_primary' => false,
];
// =========================================================================
// 관계 정의
// =========================================================================
/**
* 담당자
*/
public function assignedUser(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_user_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 getMaskedAccountNumber(): string
{
$length = strlen($this->account_number);
if ($length <= 4) {
return $this->account_number;
}
$visibleEnd = substr($this->account_number, -4);
$maskedPart = str_repeat('*', $length - 4);
return $maskedPart.$visibleEnd;
}
/**
* 활성 상태 여부
*/
public function isActive(): bool
{
return $this->status === 'active';
}
/**
* 상태 토글
*/
public function toggleStatus(): void
{
$this->status = $this->status === 'active' ? 'inactive' : 'active';
}
/**
* 대표계좌로 설정
*/
public function setAsPrimary(): void
{
$this->is_primary = true;
}
}

156
app/Models/Tenants/Card.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
namespace App\Models\Tenants;
use App\Models\Members\User;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
/**
* 카드 모델
*
* @property int $id
* @property int $tenant_id
* @property string $card_company
* @property string $card_number_encrypted
* @property string $card_number_last4
* @property string $expiry_date
* @property string|null $card_password_encrypted
* @property string $card_name
* @property string $status
* @property int|null $assigned_user_id
* @property int|null $created_by
* @property int|null $updated_by
* @property int|null $deleted_by
*/
class Card extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $table = 'cards';
protected $fillable = [
'tenant_id',
'card_company',
'card_number_encrypted',
'card_number_last4',
'expiry_date',
'card_password_encrypted',
'card_name',
'status',
'assigned_user_id',
'created_by',
'updated_by',
'deleted_by',
];
protected $hidden = [
'card_number_encrypted',
'card_password_encrypted',
];
protected $attributes = [
'status' => 'active',
];
// =========================================================================
// 관계 정의
// =========================================================================
/**
* 담당자
*/
public function assignedUser(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_user_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 setCardNumber(string $cardNumber): void
{
$this->card_number_encrypted = Crypt::encryptString($cardNumber);
$this->card_number_last4 = substr($cardNumber, -4);
}
/**
* 카드번호 복호화 조회
*/
public function getDecryptedCardNumber(): string
{
return Crypt::decryptString($this->card_number_encrypted);
}
/**
* 카드 비밀번호 암호화 설정
*/
public function setCardPassword(?string $password): void
{
$this->card_password_encrypted = $password
? Crypt::encryptString($password)
: null;
}
/**
* 카드 비밀번호 복호화 조회
*/
public function getDecryptedCardPassword(): ?string
{
return $this->card_password_encrypted
? Crypt::decryptString($this->card_password_encrypted)
: null;
}
// =========================================================================
// 헬퍼 메서드
// =========================================================================
/**
* 마스킹된 카드번호 조회
*/
public function getMaskedCardNumber(): string
{
return '****-****-****-'.$this->card_number_last4;
}
/**
* 활성 상태 여부
*/
public function isActive(): bool
{
return $this->status === 'active';
}
/**
* 상태 토글
*/
public function toggleStatus(): void
{
$this->status = $this->status === 'active' ? 'inactive' : 'active';
}
}

View File

@@ -0,0 +1,244 @@
<?php
namespace App\Services;
use App\Models\Tenants\BankAccount;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class BankAccountService extends Service
{
/**
* 계좌 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = BankAccount::query()
->where('tenant_id', $tenantId);
// 검색 필터
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('account_name', 'like', "%{$search}%")
->orWhere('bank_name', 'like', "%{$search}%")
->orWhere('account_holder', 'like', "%{$search}%")
->orWhere('account_number', 'like', "%{$search}%");
});
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 담당자 필터
if (! empty($params['assigned_user_id'])) {
$query->where('assigned_user_id', $params['assigned_user_id']);
}
// 대표계좌만 필터
if (isset($params['is_primary']) && $params['is_primary']) {
$query->where('is_primary', true);
}
// 정렬: 대표계좌 먼저, 그 다음 생성일순
$query->orderByDesc('is_primary')
->orderBy($params['sort_by'] ?? 'created_at', $params['sort_dir'] ?? 'desc');
// 페이지네이션
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 계좌 상세 조회
*/
public function show(int $id): BankAccount
{
$tenantId = $this->tenantId();
return BankAccount::query()
->where('tenant_id', $tenantId)
->with(['assignedUser:id,name'])
->findOrFail($id);
}
/**
* 계좌 등록
*/
public function store(array $data): BankAccount
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
// 첫 번째 계좌인 경우 자동으로 대표계좌 설정
$isFirst = BankAccount::where('tenant_id', $tenantId)->count() === 0;
$isPrimary = $data['is_primary'] ?? $isFirst;
// 대표계좌로 설정 시 기존 대표계좌 해제
if ($isPrimary) {
BankAccount::where('tenant_id', $tenantId)
->where('is_primary', true)
->update(['is_primary' => false]);
}
$account = BankAccount::create([
'tenant_id' => $tenantId,
'bank_code' => $data['bank_code'],
'bank_name' => $data['bank_name'],
'account_number' => $data['account_number'],
'account_holder' => $data['account_holder'],
'account_name' => $data['account_name'],
'status' => $data['status'] ?? 'active',
'assigned_user_id' => $data['assigned_user_id'] ?? null,
'is_primary' => $isPrimary,
'created_by' => $userId,
'updated_by' => $userId,
]);
return $account;
});
}
/**
* 계좌 수정
*/
public function update(int $id, array $data): BankAccount
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$account = BankAccount::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$account->fill([
'bank_code' => $data['bank_code'] ?? $account->bank_code,
'bank_name' => $data['bank_name'] ?? $account->bank_name,
'account_number' => $data['account_number'] ?? $account->account_number,
'account_holder' => $data['account_holder'] ?? $account->account_holder,
'account_name' => $data['account_name'] ?? $account->account_name,
'status' => $data['status'] ?? $account->status,
'assigned_user_id' => $data['assigned_user_id'] ?? $account->assigned_user_id,
'updated_by' => $userId,
]);
$account->save();
return $account->fresh();
});
}
/**
* 계좌 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$account = BankAccount::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
// 대표계좌 삭제 시 다른 계좌를 대표로 설정
if ($account->is_primary) {
$nextPrimary = BankAccount::where('tenant_id', $tenantId)
->where('id', '!=', $id)
->where('status', 'active')
->first();
if ($nextPrimary) {
$nextPrimary->is_primary = true;
$nextPrimary->save();
}
}
$account->deleted_by = $userId;
$account->save();
$account->delete();
return true;
});
}
/**
* 계좌 상태 토글 (사용/정지)
*/
public function toggleStatus(int $id): BankAccount
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$account = BankAccount::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$account->toggleStatus();
$account->updated_by = $userId;
$account->save();
return $account;
});
}
/**
* 대표계좌 설정
*/
public function setPrimary(int $id): BankAccount
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
// 기존 대표계좌 해제
BankAccount::where('tenant_id', $tenantId)
->where('is_primary', true)
->update(['is_primary' => false]);
// 새 대표계좌 설정
$account = BankAccount::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$account->is_primary = true;
$account->updated_by = $userId;
$account->save();
return $account;
});
}
/**
* 활성 계좌 목록 조회 (셀렉트박스용)
*/
public function getActiveAccounts(): array
{
$tenantId = $this->tenantId();
return BankAccount::query()
->where('tenant_id', $tenantId)
->where('status', 'active')
->orderByDesc('is_primary')
->orderBy('account_name')
->get(['id', 'account_name', 'bank_name', 'account_number', 'is_primary'])
->map(function ($account) {
return [
'id' => $account->id,
'account_name' => $account->account_name,
'bank_name' => $account->bank_name,
'display_number' => $account->getMaskedAccountNumber(),
'is_primary' => $account->is_primary,
];
})
->toArray();
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace App\Services;
use App\Models\Tenants\Card;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class CardService extends Service
{
/**
* 카드 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Card::query()
->where('tenant_id', $tenantId);
// 검색 필터
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('card_name', 'like', "%{$search}%")
->orWhere('card_company', 'like', "%{$search}%")
->orWhere('card_number_last4', 'like', "%{$search}%");
});
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 담당자 필터
if (! empty($params['assigned_user_id'])) {
$query->where('assigned_user_id', $params['assigned_user_id']);
}
// 정렬
$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 show(int $id): Card
{
$tenantId = $this->tenantId();
return Card::query()
->where('tenant_id', $tenantId)
->with(['assignedUser:id,name'])
->findOrFail($id);
}
/**
* 카드 등록
*/
public function store(array $data): Card
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
$card = new Card;
$card->tenant_id = $tenantId;
$card->card_company = $data['card_company'];
$card->setCardNumber($data['card_number']);
$card->expiry_date = $data['expiry_date'];
$card->card_name = $data['card_name'];
$card->status = $data['status'] ?? 'active';
$card->assigned_user_id = $data['assigned_user_id'] ?? null;
$card->created_by = $userId;
$card->updated_by = $userId;
if (! empty($data['card_password'])) {
$card->setCardPassword($data['card_password']);
}
$card->save();
return $card;
});
}
/**
* 카드 수정
*/
public function update(int $id, array $data): Card
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$card = Card::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (isset($data['card_company'])) {
$card->card_company = $data['card_company'];
}
if (isset($data['card_number'])) {
$card->setCardNumber($data['card_number']);
}
if (isset($data['expiry_date'])) {
$card->expiry_date = $data['expiry_date'];
}
if (isset($data['card_name'])) {
$card->card_name = $data['card_name'];
}
if (isset($data['status'])) {
$card->status = $data['status'];
}
if (array_key_exists('assigned_user_id', $data)) {
$card->assigned_user_id = $data['assigned_user_id'];
}
if (isset($data['card_password'])) {
$card->setCardPassword($data['card_password']);
}
$card->updated_by = $userId;
$card->save();
return $card->fresh();
});
}
/**
* 카드 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$card = Card::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$card->deleted_by = $userId;
$card->save();
$card->delete();
return true;
});
}
/**
* 카드 상태 토글 (사용/정지)
*/
public function toggleStatus(int $id): Card
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$card = Card::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$card->toggleStatus();
$card->updated_by = $userId;
$card->save();
return $card;
});
}
/**
* 활성 카드 목록 조회 (셀렉트박스용)
*/
public function getActiveCards(): array
{
$tenantId = $this->tenantId();
return Card::query()
->where('tenant_id', $tenantId)
->where('status', 'active')
->orderBy('card_name')
->get(['id', 'card_name', 'card_company', 'card_number_last4'])
->map(function ($card) {
return [
'id' => $card->id,
'card_name' => $card->card_name,
'card_company' => $card->card_company,
'display_number' => '****-'.$card->card_number_last4,
];
})
->toArray();
}
}

View File

@@ -0,0 +1,353 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(name="BankAccounts", description="계좌 관리")
*
* @OA\Schema(
* schema="BankAccount",
* type="object",
* description="계좌 정보",
*
* @OA\Property(property="id", type="integer", example=1, description="계좌 ID"),
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
* @OA\Property(property="bank_code", type="string", example="088", description="은행 코드"),
* @OA\Property(property="bank_name", type="string", example="신한은행", description="은행명"),
* @OA\Property(property="account_number", type="string", example="110-123-456789", description="계좌번호"),
* @OA\Property(property="account_holder", type="string", example="주식회사 샘", description="예금주"),
* @OA\Property(property="account_name", type="string", example="운영계좌", description="계좌 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID"),
* @OA\Property(property="assigned_user", type="object", nullable=true,
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="홍길동"),
* description="담당자 정보"
* ),
* @OA\Property(property="is_primary", type="boolean", example=true, description="대표계좌 여부"),
* @OA\Property(property="created_by", type="integer", example=1, nullable=true, description="생성자 ID"),
* @OA\Property(property="updated_by", type="integer", example=1, nullable=true, description="수정자 ID"),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="updated_at", type="string", format="date-time")
* )
*
* @OA\Schema(
* schema="BankAccountCreateRequest",
* type="object",
* required={"bank_code","bank_name","account_number","account_holder","account_name"},
* description="계좌 등록 요청",
*
* @OA\Property(property="bank_code", type="string", example="088", maxLength=10, description="은행 코드"),
* @OA\Property(property="bank_name", type="string", example="신한은행", maxLength=50, description="은행명"),
* @OA\Property(property="account_number", type="string", example="110-123-456789", maxLength=30, description="계좌번호"),
* @OA\Property(property="account_holder", type="string", example="주식회사 샘", maxLength=50, description="예금주"),
* @OA\Property(property="account_name", type="string", example="운영계좌", maxLength=100, description="계좌 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID"),
* @OA\Property(property="is_primary", type="boolean", example=false, description="대표계좌 여부")
* )
*
* @OA\Schema(
* schema="BankAccountUpdateRequest",
* type="object",
* description="계좌 수정 요청",
*
* @OA\Property(property="bank_code", type="string", example="088", maxLength=10, description="은행 코드"),
* @OA\Property(property="bank_name", type="string", example="신한은행", maxLength=50, description="은행명"),
* @OA\Property(property="account_number", type="string", example="110-123-456789", maxLength=30, description="계좌번호"),
* @OA\Property(property="account_holder", type="string", example="주식회사 샘", maxLength=50, description="예금주"),
* @OA\Property(property="account_name", type="string", example="운영계좌", maxLength=100, description="계좌 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID")
* )
*
* @OA\Schema(
* schema="BankAccountListItem",
* type="object",
* description="계좌 목록 아이템 (셀렉트박스용)",
*
* @OA\Property(property="id", type="integer", example=1, description="계좌 ID"),
* @OA\Property(property="account_name", type="string", example="운영계좌", description="계좌 별칭"),
* @OA\Property(property="bank_name", type="string", example="신한은행", description="은행명"),
* @OA\Property(property="display_number", type="string", example="*****6789", description="마스킹된 계좌번호"),
* @OA\Property(property="is_primary", type="boolean", example=true, description="대표계좌 여부")
* )
*/
class BankAccountApi
{
/**
* @OA\Get(
* path="/api/v1/bank-accounts",
* tags={"BankAccounts"},
* summary="계좌 목록 조회",
* description="계좌 목록을 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="search", in="query", description="검색어 (계좌명, 은행명, 예금주, 계좌번호)", @OA\Schema(type="string")),
* @OA\Parameter(name="status", in="query", description="상태 필터", @OA\Schema(type="string", enum={"active","inactive"})),
* @OA\Parameter(name="assigned_user_id", in="query", description="담당자 ID", @OA\Schema(type="integer")),
* @OA\Parameter(name="is_primary", in="query", description="대표계좌만", @OA\Schema(type="boolean")),
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"account_name","bank_name","created_at"}, default="created_at")),
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
*
* @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/BankAccount")),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=10)
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function index() {}
/**
* @OA\Post(
* path="/api/v1/bank-accounts",
* tags={"BankAccounts"},
* summary="계좌 등록",
* description="새로운 계좌를 등록합니다. 첫 번째 계좌는 자동으로 대표계좌로 설정됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/BankAccountCreateRequest")
* ),
*
* @OA\Response(
* response=201,
* description="등록 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/BankAccount")
* )
* }
* )
* ),
*
* @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=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function store() {}
/**
* @OA\Get(
* path="/api/v1/bank-accounts/active",
* tags={"BankAccounts"},
* summary="활성 계좌 목록 (셀렉트박스용)",
* description="활성 상태의 계좌 목록을 간단한 형태로 조회합니다.",
* 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/BankAccountListItem"))
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function active() {}
/**
* @OA\Get(
* path="/api/v1/bank-accounts/{id}",
* tags={"BankAccounts"},
* summary="계좌 상세 조회",
* description="계좌 상세 정보를 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="계좌 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/BankAccount")
* )
* }
* )
* ),
*
* @OA\Response(response=401, 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 show() {}
/**
* @OA\Put(
* path="/api/v1/bank-accounts/{id}",
* tags={"BankAccounts"},
* summary="계좌 수정",
* description="계좌 정보를 수정합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="계좌 ID", @OA\Schema(type="integer")),
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/BankAccountUpdateRequest")
* ),
*
* @OA\Response(
* response=200,
* description="수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/BankAccount")
* )
* }
* )
* ),
*
* @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=404, description="계좌 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function update() {}
/**
* @OA\Delete(
* path="/api/v1/bank-accounts/{id}",
* tags={"BankAccounts"},
* summary="계좌 삭제",
* description="계좌를 삭제합니다. (Soft Delete) 대표계좌 삭제 시 다른 활성 계좌가 대표계좌로 자동 설정됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="계좌 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="삭제 성공",
*
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
* ),
*
* @OA\Response(response=401, 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 destroy() {}
/**
* @OA\Patch(
* path="/api/v1/bank-accounts/{id}/toggle",
* tags={"BankAccounts"},
* summary="계좌 상태 토글",
* description="계좌의 상태를 토글합니다. (active ↔ inactive)",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="계좌 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="토글 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/BankAccount")
* )
* }
* )
* ),
*
* @OA\Response(response=401, 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 toggle() {}
/**
* @OA\Patch(
* path="/api/v1/bank-accounts/{id}/set-primary",
* tags={"BankAccounts"},
* summary="대표계좌 설정",
* description="해당 계좌를 대표계좌로 설정합니다. 기존 대표계좌는 자동으로 해제됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="계좌 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="설정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/BankAccount")
* )
* }
* )
* ),
*
* @OA\Response(response=401, 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 setPrimary() {}
}

315
app/Swagger/v1/CardApi.php Normal file
View File

@@ -0,0 +1,315 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(name="Cards", description="카드 관리")
*
* @OA\Schema(
* schema="Card",
* type="object",
* description="카드 정보",
*
* @OA\Property(property="id", type="integer", example=1, description="카드 ID"),
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
* @OA\Property(property="card_company", type="string", example="삼성카드", description="카드사"),
* @OA\Property(property="card_number_last4", type="string", example="1234", description="카드번호 끝 4자리"),
* @OA\Property(property="expiry_date", type="string", example="12/25", description="유효기간 (MM/YY)"),
* @OA\Property(property="card_name", type="string", example="법인카드 1", description="카드 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID"),
* @OA\Property(property="assigned_user", type="object", nullable=true,
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="홍길동"),
* description="담당자 정보"
* ),
* @OA\Property(property="created_by", type="integer", example=1, nullable=true, description="생성자 ID"),
* @OA\Property(property="updated_by", type="integer", example=1, nullable=true, description="수정자 ID"),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="updated_at", type="string", format="date-time")
* )
*
* @OA\Schema(
* schema="CardCreateRequest",
* type="object",
* required={"card_company","card_number","expiry_date","card_name"},
* description="카드 등록 요청",
*
* @OA\Property(property="card_company", type="string", example="삼성카드", maxLength=50, description="카드사"),
* @OA\Property(property="card_number", type="string", example="1234567890123456", description="카드번호 (13-19자리)"),
* @OA\Property(property="expiry_date", type="string", example="12/25", pattern="^(0[1-9]|1[0-2])/\\d{2}$", description="유효기간 (MM/YY)"),
* @OA\Property(property="card_password", type="string", example="12", maxLength=2, description="비밀번호 앞 2자리 (선택)"),
* @OA\Property(property="card_name", type="string", example="법인카드 1", maxLength=100, description="카드 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID")
* )
*
* @OA\Schema(
* schema="CardUpdateRequest",
* type="object",
* description="카드 수정 요청",
*
* @OA\Property(property="card_company", type="string", example="삼성카드", maxLength=50, description="카드사"),
* @OA\Property(property="card_number", type="string", example="1234567890123456", description="카드번호 (변경 시)"),
* @OA\Property(property="expiry_date", type="string", example="12/25", pattern="^(0[1-9]|1[0-2])/\\d{2}$", description="유효기간 (MM/YY)"),
* @OA\Property(property="card_password", type="string", example="12", maxLength=2, description="비밀번호 앞 2자리"),
* @OA\Property(property="card_name", type="string", example="법인카드 1", maxLength=100, description="카드 별칭"),
* @OA\Property(property="status", type="string", enum={"active","inactive"}, example="active", description="상태"),
* @OA\Property(property="assigned_user_id", type="integer", example=1, nullable=true, description="담당자 ID")
* )
*
* @OA\Schema(
* schema="CardListItem",
* type="object",
* description="카드 목록 아이템 (셀렉트박스용)",
*
* @OA\Property(property="id", type="integer", example=1, description="카드 ID"),
* @OA\Property(property="card_name", type="string", example="법인카드 1", description="카드 별칭"),
* @OA\Property(property="card_company", type="string", example="삼성카드", description="카드사"),
* @OA\Property(property="display_number", type="string", example="****-1234", description="마스킹된 카드번호")
* )
*/
class CardApi
{
/**
* @OA\Get(
* path="/api/v1/cards",
* tags={"Cards"},
* summary="카드 목록 조회",
* description="카드 목록을 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="search", in="query", description="검색어 (카드명, 카드사, 끝4자리)", @OA\Schema(type="string")),
* @OA\Parameter(name="status", in="query", description="상태 필터", @OA\Schema(type="string", enum={"active","inactive"})),
* @OA\Parameter(name="assigned_user_id", in="query", description="담당자 ID", @OA\Schema(type="integer")),
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"card_name","card_company","created_at"}, default="created_at")),
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
*
* @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/Card")),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=10)
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function index() {}
/**
* @OA\Post(
* path="/api/v1/cards",
* tags={"Cards"},
* summary="카드 등록",
* description="새로운 카드를 등록합니다. 카드번호는 암호화되어 저장됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/CardCreateRequest")
* ),
*
* @OA\Response(
* response=201,
* description="등록 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Card")
* )
* }
* )
* ),
*
* @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=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function store() {}
/**
* @OA\Get(
* path="/api/v1/cards/active",
* tags={"Cards"},
* summary="활성 카드 목록 (셀렉트박스용)",
* description="활성 상태의 카드 목록을 간단한 형태로 조회합니다.",
* 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/CardListItem"))
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function active() {}
/**
* @OA\Get(
* path="/api/v1/cards/{id}",
* tags={"Cards"},
* summary="카드 상세 조회",
* description="카드 상세 정보를 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="카드 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Card")
* )
* }
* )
* ),
*
* @OA\Response(response=401, 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 show() {}
/**
* @OA\Put(
* path="/api/v1/cards/{id}",
* tags={"Cards"},
* summary="카드 수정",
* description="카드 정보를 수정합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="카드 ID", @OA\Schema(type="integer")),
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/CardUpdateRequest")
* ),
*
* @OA\Response(
* response=200,
* description="수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Card")
* )
* }
* )
* ),
*
* @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=404, description="카드 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function update() {}
/**
* @OA\Delete(
* path="/api/v1/cards/{id}",
* tags={"Cards"},
* summary="카드 삭제",
* description="카드를 삭제합니다. (Soft Delete)",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="카드 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="삭제 성공",
*
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
* ),
*
* @OA\Response(response=401, 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 destroy() {}
/**
* @OA\Patch(
* path="/api/v1/cards/{id}/toggle",
* tags={"Cards"},
* summary="카드 상태 토글",
* description="카드의 상태를 토글합니다. (active ↔ inactive)",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="카드 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="토글 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Card")
* )
* }
* )
* ),
*
* @OA\Response(response=401, 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 toggle() {}
}

View File

@@ -0,0 +1,43 @@
<?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('cards', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('card_company', 50)->comment('카드사');
$table->text('card_number_encrypted')->comment('암호화된 카드번호');
$table->string('card_number_last4', 4)->comment('카드번호 끝 4자리');
$table->string('expiry_date', 5)->comment('유효기간 (MM/YY)');
$table->text('card_password_encrypted')->nullable()->comment('암호화된 비밀번호 앞2자리');
$table->string('card_name', 100)->comment('카드 별칭');
$table->string('status', 20)->default('active')->comment('상태: active/inactive');
$table->unsignedBigInteger('assigned_user_id')->nullable()->comment('담당자 ID');
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
$table->softDeletes();
$table->timestamps();
$table->index('tenant_id', 'idx_cards_tenant');
$table->index('status', 'idx_cards_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cards');
}
};

View File

@@ -0,0 +1,43 @@
<?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('bank_accounts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('bank_code', 10)->comment('은행 코드');
$table->string('bank_name', 50)->comment('은행명');
$table->string('account_number', 30)->comment('계좌번호');
$table->string('account_holder', 50)->comment('예금주');
$table->string('account_name', 100)->comment('계좌 별칭');
$table->string('status', 20)->default('active')->comment('상태: active/inactive');
$table->unsignedBigInteger('assigned_user_id')->nullable()->comment('담당자 ID');
$table->boolean('is_primary')->default(false)->comment('대표계좌 여부');
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
$table->softDeletes();
$table->timestamps();
$table->index('tenant_id', 'idx_bank_accounts_tenant');
$table->index('status', 'idx_bank_accounts_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bank_accounts');
}
};

View File

@@ -49,6 +49,8 @@
use App\Http\Controllers\Api\V1\RegisterController; use App\Http\Controllers\Api\V1\RegisterController;
use App\Http\Controllers\Api\V1\RoleController; use App\Http\Controllers\Api\V1\RoleController;
use App\Http\Controllers\Api\V1\RolePermissionController; use App\Http\Controllers\Api\V1\RolePermissionController;
use App\Http\Controllers\Api\V1\BankAccountController;
use App\Http\Controllers\Api\V1\CardController;
use App\Http\Controllers\Api\V1\SiteController; use App\Http\Controllers\Api\V1\SiteController;
use App\Http\Controllers\Api\V1\TenantController; use App\Http\Controllers\Api\V1\TenantController;
use App\Http\Controllers\Api\V1\TenantFieldSettingController; use App\Http\Controllers\Api\V1\TenantFieldSettingController;
@@ -269,6 +271,29 @@
Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy'); Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy');
}); });
// Card API (카드 관리)
Route::prefix('cards')->group(function () {
Route::get('', [CardController::class, 'index'])->name('v1.cards.index');
Route::post('', [CardController::class, 'store'])->name('v1.cards.store');
Route::get('/active', [CardController::class, 'active'])->name('v1.cards.active');
Route::get('/{id}', [CardController::class, 'show'])->whereNumber('id')->name('v1.cards.show');
Route::put('/{id}', [CardController::class, 'update'])->whereNumber('id')->name('v1.cards.update');
Route::delete('/{id}', [CardController::class, 'destroy'])->whereNumber('id')->name('v1.cards.destroy');
Route::patch('/{id}/toggle', [CardController::class, 'toggle'])->whereNumber('id')->name('v1.cards.toggle');
});
// BankAccount API (계좌 관리)
Route::prefix('bank-accounts')->group(function () {
Route::get('', [BankAccountController::class, 'index'])->name('v1.bank-accounts.index');
Route::post('', [BankAccountController::class, 'store'])->name('v1.bank-accounts.store');
Route::get('/active', [BankAccountController::class, 'active'])->name('v1.bank-accounts.active');
Route::get('/{id}', [BankAccountController::class, 'show'])->whereNumber('id')->name('v1.bank-accounts.show');
Route::put('/{id}', [BankAccountController::class, 'update'])->whereNumber('id')->name('v1.bank-accounts.update');
Route::delete('/{id}', [BankAccountController::class, 'destroy'])->whereNumber('id')->name('v1.bank-accounts.destroy');
Route::patch('/{id}/toggle', [BankAccountController::class, 'toggle'])->whereNumber('id')->name('v1.bank-accounts.toggle');
Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary');
});
// Permission API // Permission API
Route::prefix('permissions')->group(function () { Route::prefix('permissions')->group(function () {
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스 Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스