feat: I-1 미지급비용 관리 API 개발

- ExpectedExpense 모델 및 마이그레이션 생성
- ExpectedExpenseService 구현 (CRUD, 일괄삭제, 지급일 변경, 요약)
- ExpectedExpenseController REST API 구현
- FormRequest 검증 클래스 3개 생성
- Swagger API 문서 작성
- 라우트 추가 (8개 엔드포인트)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-26 13:23:07 +09:00
parent 0fef26f42a
commit 1f5539db32
9 changed files with 1097 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\ExpectedExpense\StoreExpectedExpenseRequest;
use App\Http\Requests\V1\ExpectedExpense\UpdateExpectedExpenseRequest;
use App\Http\Requests\V1\ExpectedExpense\UpdateExpectedPaymentDateRequest;
use App\Services\ExpectedExpenseService;
use Illuminate\Http\Request;
class ExpectedExpenseController extends Controller
{
public function __construct(
private readonly ExpectedExpenseService $service
) {}
/**
* 미지급비용 목록
*/
public function index(Request $request)
{
$params = $request->only([
'search',
'start_date',
'end_date',
'client_id',
'transaction_type',
'payment_status',
'approval_status',
'sort_by',
'sort_dir',
'per_page',
'page',
]);
$expenses = $this->service->index($params);
return ApiResponse::success($expenses, __('message.fetched'));
}
/**
* 미지급비용 등록
*/
public function store(StoreExpectedExpenseRequest $request)
{
$expense = $this->service->store($request->validated());
return ApiResponse::success($expense, __('message.created'), [], 201);
}
/**
* 미지급비용 상세
*/
public function show(int $id)
{
$expense = $this->service->show($id);
return ApiResponse::success($expense, __('message.fetched'));
}
/**
* 미지급비용 수정
*/
public function update(int $id, UpdateExpectedExpenseRequest $request)
{
$expense = $this->service->update($id, $request->validated());
return ApiResponse::success($expense, __('message.updated'));
}
/**
* 미지급비용 삭제
*/
public function destroy(int $id)
{
$this->service->destroy($id);
return ApiResponse::success(null, __('message.deleted'));
}
/**
* 미지급비용 일괄 삭제
*/
public function destroyMany(Request $request)
{
$ids = $request->input('ids', []);
if (empty($ids)) {
return ApiResponse::error(__('error.no_ids_provided'), 400);
}
$count = $this->service->destroyMany($ids);
return ApiResponse::success(['deleted_count' => $count], __('message.deleted'));
}
/**
* 예상 지급일 일괄 변경
*/
public function updateExpectedPaymentDate(UpdateExpectedPaymentDateRequest $request)
{
$count = $this->service->updateExpectedPaymentDate(
$request->input('ids'),
$request->input('expected_payment_date')
);
return ApiResponse::success(['updated_count' => $count], __('message.updated'));
}
/**
* 미지급비용 요약 (기간별 합계)
*/
public function summary(Request $request)
{
$params = $request->only([
'start_date',
'end_date',
'payment_status',
]);
$summary = $this->service->summary($params);
return ApiResponse::success($summary, __('message.fetched'));
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests\V1\ExpectedExpense;
use Illuminate\Foundation\Http\FormRequest;
class StoreExpectedExpenseRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'expected_payment_date' => ['required', 'date'],
'settlement_date' => ['nullable', 'date'],
'transaction_type' => ['required', 'string', 'in:purchase,advance,suspense,rent,salary,insurance,tax,utilities,other'],
'amount' => ['required', 'numeric', 'min:0'],
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
'client_name' => ['nullable', 'string', 'max:100'],
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
'account_code' => ['nullable', 'string', 'max:50'],
'payment_status' => ['nullable', 'string', 'in:pending,partial,paid,overdue'],
'approval_status' => ['nullable', 'string', 'in:none,pending,approved,rejected'],
'description' => ['nullable', 'string', 'max:1000'],
];
}
public function messages(): array
{
return [
'expected_payment_date.required' => __('validation.required', ['attribute' => '예상 지급일']),
'transaction_type.required' => __('validation.required', ['attribute' => '거래유형']),
'transaction_type.in' => __('validation.in', ['attribute' => '거래유형']),
'amount.required' => __('validation.required', ['attribute' => '금액']),
'amount.min' => __('validation.min.numeric', ['attribute' => '금액', 'min' => 0]),
];
}
public function attributes(): array
{
return [
'expected_payment_date' => '예상 지급일',
'settlement_date' => '결제일',
'transaction_type' => '거래유형',
'amount' => '금액',
'client_id' => '거래처',
'client_name' => '거래처명',
'bank_account_id' => '계좌',
'account_code' => '계정과목',
'payment_status' => '지급상태',
'approval_status' => '결재상태',
'description' => '적요',
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\V1\ExpectedExpense;
use Illuminate\Foundation\Http\FormRequest;
class UpdateExpectedExpenseRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'expected_payment_date' => ['sometimes', 'date'],
'settlement_date' => ['nullable', 'date'],
'transaction_type' => ['sometimes', 'string', 'in:purchase,advance,suspense,rent,salary,insurance,tax,utilities,other'],
'amount' => ['sometimes', 'numeric', 'min:0'],
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
'client_name' => ['nullable', 'string', 'max:100'],
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
'account_code' => ['nullable', 'string', 'max:50'],
'payment_status' => ['sometimes', 'string', 'in:pending,partial,paid,overdue'],
'approval_status' => ['sometimes', 'string', 'in:none,pending,approved,rejected'],
'description' => ['nullable', 'string', 'max:1000'],
];
}
public function attributes(): array
{
return [
'expected_payment_date' => '예상 지급일',
'settlement_date' => '결제일',
'transaction_type' => '거래유형',
'amount' => '금액',
'client_id' => '거래처',
'client_name' => '거래처명',
'bank_account_id' => '계좌',
'account_code' => '계정과목',
'payment_status' => '지급상태',
'approval_status' => '결재상태',
'description' => '적요',
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\V1\ExpectedExpense;
use Illuminate\Foundation\Http\FormRequest;
class UpdateExpectedPaymentDateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['required', 'integer'],
'expected_payment_date' => ['required', 'date'],
];
}
public function messages(): array
{
return [
'ids.required' => __('validation.required', ['attribute' => '대상 ID']),
'ids.min' => __('validation.min.array', ['attribute' => '대상 ID', 'min' => 1]),
'expected_payment_date.required' => __('validation.required', ['attribute' => '예상 지급일']),
];
}
public function attributes(): array
{
return [
'ids' => '대상 ID',
'expected_payment_date' => '예상 지급일',
];
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Models\Tenants;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class ExpectedExpense extends Model
{
use BelongsToTenant, SoftDeletes;
protected $fillable = [
'tenant_id',
'expected_payment_date',
'settlement_date',
'transaction_type',
'amount',
'client_id',
'client_name',
'bank_account_id',
'account_code',
'payment_status',
'approval_status',
'description',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'expected_payment_date' => 'date',
'settlement_date' => 'date',
'amount' => 'decimal:2',
'client_id' => 'integer',
'bank_account_id' => 'integer',
];
/**
* 거래유형 목록
*/
public const TRANSACTION_TYPES = [
'purchase' => '매입',
'advance' => '선급금',
'suspense' => '가지급금',
'rent' => '임대료',
'salary' => '급여',
'insurance' => '보험료',
'tax' => '세금',
'utilities' => '공과금',
'other' => '기타',
];
/**
* 지급상태 목록
*/
public const PAYMENT_STATUSES = [
'pending' => '미지급',
'partial' => '부분지급',
'paid' => '지급완료',
'overdue' => '연체',
];
/**
* 결재상태 목록
*/
public const APPROVAL_STATUSES = [
'none' => '미신청',
'pending' => '결재대기',
'approved' => '결재완료',
'rejected' => '반려',
];
/**
* 거래처 관계
*/
public function client(): BelongsTo
{
return $this->belongsTo(\App\Models\Orders\Client::class);
}
/**
* 계좌 관계
*/
public function bankAccount(): BelongsTo
{
return $this->belongsTo(BankAccount::class);
}
/**
* 생성자 관계
*/
public function creator(): BelongsTo
{
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
}
/**
* 거래처명 조회 (회원/비회원 통합)
*/
public function getDisplayClientNameAttribute(): string
{
if ($this->client) {
return $this->client->name;
}
return $this->client_name ?? '';
}
/**
* 거래유형 라벨
*/
public function getTransactionTypeLabelAttribute(): string
{
return self::TRANSACTION_TYPES[$this->transaction_type] ?? $this->transaction_type;
}
/**
* 지급상태 라벨
*/
public function getPaymentStatusLabelAttribute(): string
{
return self::PAYMENT_STATUSES[$this->payment_status] ?? $this->payment_status;
}
/**
* 결재상태 라벨
*/
public function getApprovalStatusLabelAttribute(): string
{
return self::APPROVAL_STATUSES[$this->approval_status] ?? $this->approval_status;
}
}

View File

@@ -0,0 +1,302 @@
<?php
namespace App\Services;
use App\Models\Tenants\ExpectedExpense;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class ExpectedExpenseService extends Service
{
/**
* 미지급비용 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = ExpectedExpense::query()
->where('tenant_id', $tenantId)
->with(['client:id,name', 'bankAccount:id,bank_name,account_name']);
// 검색어 필터
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('client_name', 'like', "%{$search}%")
->orWhere('account_code', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
->orWhereHas('client', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
});
}
// 날짜 범위 필터 (예상 지급일 기준)
if (! empty($params['start_date'])) {
$query->where('expected_payment_date', '>=', $params['start_date']);
}
if (! empty($params['end_date'])) {
$query->where('expected_payment_date', '<=', $params['end_date']);
}
// 거래처 필터
if (! empty($params['client_id'])) {
$query->where('client_id', $params['client_id']);
}
// 거래유형 필터
if (! empty($params['transaction_type'])) {
$query->where('transaction_type', $params['transaction_type']);
}
// 지급상태 필터
if (! empty($params['payment_status'])) {
$query->where('payment_status', $params['payment_status']);
}
// 결재상태 필터
if (! empty($params['approval_status'])) {
$query->where('approval_status', $params['approval_status']);
}
// 정렬
$sortBy = $params['sort_by'] ?? 'expected_payment_date';
$sortDir = $params['sort_dir'] ?? 'asc';
$query->orderBy($sortBy, $sortDir);
// 페이지네이션
$perPage = $params['per_page'] ?? 50;
return $query->paginate($perPage);
}
/**
* 미지급비용 상세 조회
*/
public function show(int $id): ExpectedExpense
{
$tenantId = $this->tenantId();
return ExpectedExpense::query()
->where('tenant_id', $tenantId)
->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'creator:id,name'])
->findOrFail($id);
}
/**
* 미지급비용 등록
*/
public function store(array $data): ExpectedExpense
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
$expense = new ExpectedExpense;
$expense->tenant_id = $tenantId;
$expense->expected_payment_date = $data['expected_payment_date'];
$expense->settlement_date = $data['settlement_date'] ?? null;
$expense->transaction_type = $data['transaction_type'];
$expense->amount = $data['amount'];
$expense->client_id = $data['client_id'] ?? null;
$expense->client_name = $data['client_name'] ?? null;
$expense->bank_account_id = $data['bank_account_id'] ?? null;
$expense->account_code = $data['account_code'] ?? null;
$expense->payment_status = $data['payment_status'] ?? 'pending';
$expense->approval_status = $data['approval_status'] ?? 'none';
$expense->description = $data['description'] ?? null;
$expense->created_by = $userId;
$expense->updated_by = $userId;
$expense->save();
return $expense->load(['client:id,name', 'bankAccount:id,bank_name,account_name']);
});
}
/**
* 미지급비용 수정
*/
public function update(int $id, array $data): ExpectedExpense
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$expense = ExpectedExpense::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (isset($data['expected_payment_date'])) {
$expense->expected_payment_date = $data['expected_payment_date'];
}
if (array_key_exists('settlement_date', $data)) {
$expense->settlement_date = $data['settlement_date'];
}
if (isset($data['transaction_type'])) {
$expense->transaction_type = $data['transaction_type'];
}
if (isset($data['amount'])) {
$expense->amount = $data['amount'];
}
if (array_key_exists('client_id', $data)) {
$expense->client_id = $data['client_id'];
}
if (array_key_exists('client_name', $data)) {
$expense->client_name = $data['client_name'];
}
if (array_key_exists('bank_account_id', $data)) {
$expense->bank_account_id = $data['bank_account_id'];
}
if (array_key_exists('account_code', $data)) {
$expense->account_code = $data['account_code'];
}
if (isset($data['payment_status'])) {
$expense->payment_status = $data['payment_status'];
}
if (isset($data['approval_status'])) {
$expense->approval_status = $data['approval_status'];
}
if (array_key_exists('description', $data)) {
$expense->description = $data['description'];
}
$expense->updated_by = $userId;
$expense->save();
return $expense->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name']);
});
}
/**
* 미지급비용 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$expense = ExpectedExpense::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$expense->deleted_by = $userId;
$expense->save();
$expense->delete();
return true;
});
}
/**
* 일괄 삭제
*/
public function destroyMany(array $ids): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($ids, $tenantId, $userId) {
$count = 0;
foreach ($ids as $id) {
$expense = ExpectedExpense::query()
->where('tenant_id', $tenantId)
->find($id);
if ($expense) {
$expense->deleted_by = $userId;
$expense->save();
$expense->delete();
$count++;
}
}
return $count;
});
}
/**
* 예상 지급일 일괄 변경
*/
public function updateExpectedPaymentDate(array $ids, string $newDate): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($ids, $newDate, $tenantId, $userId) {
return ExpectedExpense::query()
->where('tenant_id', $tenantId)
->whereIn('id', $ids)
->update([
'expected_payment_date' => $newDate,
'updated_by' => $userId,
]);
});
}
/**
* 미지급비용 요약 (기간별 합계)
*/
public function summary(array $params): array
{
$tenantId = $this->tenantId();
$query = ExpectedExpense::query()
->where('tenant_id', $tenantId);
// 날짜 범위 필터
if (! empty($params['start_date'])) {
$query->where('expected_payment_date', '>=', $params['start_date']);
}
if (! empty($params['end_date'])) {
$query->where('expected_payment_date', '<=', $params['end_date']);
}
// 지급상태 필터
if (! empty($params['payment_status'])) {
$query->where('payment_status', $params['payment_status']);
}
// 전체 합계
$total = (clone $query)->sum('amount');
$count = (clone $query)->count();
// 지급상태별 합계
$byPaymentStatus = (clone $query)
->select('payment_status', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('payment_status')
->get()
->keyBy('payment_status')
->toArray();
// 거래유형별 합계
$byTransactionType = (clone $query)
->select('transaction_type', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('transaction_type')
->get()
->keyBy('transaction_type')
->toArray();
// 월별 합계
$byMonth = (clone $query)
->select(
DB::raw("DATE_FORMAT(expected_payment_date, '%Y-%m') as month"),
DB::raw('SUM(amount) as total'),
DB::raw('COUNT(*) as count')
)
->groupBy('month')
->orderBy('month')
->get()
->keyBy('month')
->toArray();
return [
'total_amount' => (float) $total,
'total_count' => $count,
'by_payment_status' => $byPaymentStatus,
'by_transaction_type' => $byTransactionType,
'by_month' => $byMonth,
];
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(
* name="ExpectedExpense",
* description="미지급비용(지출예상내역) 관리"
* )
*
* @OA\Schema(
* schema="ExpectedExpense",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="expected_payment_date", type="string", format="date", example="2025-01-15"),
* @OA\Property(property="settlement_date", type="string", format="date", nullable=true, example="2025-01-10"),
* @OA\Property(property="transaction_type", type="string", enum={"purchase","advance","suspense","rent","salary","insurance","tax","utilities","other"}, example="purchase"),
* @OA\Property(property="amount", type="number", format="float", example=1500000),
* @OA\Property(property="client_id", type="integer", nullable=true, example=1),
* @OA\Property(property="client_name", type="string", nullable=true, example="(주)삼성전자"),
* @OA\Property(property="bank_account_id", type="integer", nullable=true, example=1),
* @OA\Property(property="account_code", type="string", nullable=true, example="매입비용"),
* @OA\Property(property="payment_status", type="string", enum={"pending","partial","paid","overdue"}, example="pending"),
* @OA\Property(property="approval_status", type="string", enum={"none","pending","approved","rejected"}, example="none"),
* @OA\Property(property="description", type="string", nullable=true, example="월정산"),
* @OA\Property(property="created_at", type="string", format="datetime"),
* @OA\Property(property="updated_at", type="string", format="datetime"),
* @OA\Property(
* property="client",
* type="object",
* nullable=true,
* @OA\Property(property="id", type="integer"),
* @OA\Property(property="name", type="string")
* ),
* @OA\Property(
* property="bank_account",
* type="object",
* nullable=true,
* @OA\Property(property="id", type="integer"),
* @OA\Property(property="bank_name", type="string"),
* @OA\Property(property="account_name", type="string")
* )
* )
*
* @OA\Schema(
* schema="ExpectedExpensePagination",
* type="object",
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ExpectedExpense")),
* @OA\Property(property="first_page_url", type="string"),
* @OA\Property(property="from", type="integer"),
* @OA\Property(property="last_page", type="integer"),
* @OA\Property(property="last_page_url", type="string"),
* @OA\Property(property="next_page_url", type="string", nullable=true),
* @OA\Property(property="path", type="string"),
* @OA\Property(property="per_page", type="integer"),
* @OA\Property(property="prev_page_url", type="string", nullable=true),
* @OA\Property(property="to", type="integer"),
* @OA\Property(property="total", type="integer")
* )
*
* @OA\Schema(
* schema="ExpectedExpenseCreateRequest",
* type="object",
* required={"expected_payment_date", "transaction_type", "amount"},
* @OA\Property(property="expected_payment_date", type="string", format="date", example="2025-01-15"),
* @OA\Property(property="settlement_date", type="string", format="date", nullable=true, example="2025-01-10"),
* @OA\Property(property="transaction_type", type="string", enum={"purchase","advance","suspense","rent","salary","insurance","tax","utilities","other"}, example="purchase"),
* @OA\Property(property="amount", type="number", format="float", example=1500000),
* @OA\Property(property="client_id", type="integer", nullable=true, example=1),
* @OA\Property(property="client_name", type="string", nullable=true, example="(주)삼성전자"),
* @OA\Property(property="bank_account_id", type="integer", nullable=true, example=1),
* @OA\Property(property="account_code", type="string", nullable=true, example="매입비용"),
* @OA\Property(property="payment_status", type="string", enum={"pending","partial","paid","overdue"}, example="pending"),
* @OA\Property(property="approval_status", type="string", enum={"none","pending","approved","rejected"}, example="none"),
* @OA\Property(property="description", type="string", nullable=true, example="월정산")
* )
*
* @OA\Schema(
* schema="ExpectedExpenseUpdateRequest",
* type="object",
* @OA\Property(property="expected_payment_date", type="string", format="date", example="2025-01-20"),
* @OA\Property(property="settlement_date", type="string", format="date", nullable=true),
* @OA\Property(property="transaction_type", type="string", enum={"purchase","advance","suspense","rent","salary","insurance","tax","utilities","other"}),
* @OA\Property(property="amount", type="number", format="float"),
* @OA\Property(property="client_id", type="integer", nullable=true),
* @OA\Property(property="client_name", type="string", nullable=true),
* @OA\Property(property="bank_account_id", type="integer", nullable=true),
* @OA\Property(property="account_code", type="string", nullable=true),
* @OA\Property(property="payment_status", type="string", enum={"pending","partial","paid","overdue"}),
* @OA\Property(property="approval_status", type="string", enum={"none","pending","approved","rejected"}),
* @OA\Property(property="description", type="string", nullable=true)
* )
*
* @OA\Schema(
* schema="ExpectedExpenseSummary",
* type="object",
* @OA\Property(property="total_amount", type="number", format="float", example=15000000),
* @OA\Property(property="total_count", type="integer", example=12),
* @OA\Property(property="by_payment_status", type="object"),
* @OA\Property(property="by_transaction_type", type="object"),
* @OA\Property(property="by_month", type="object")
* )
*/
class ExpectedExpenseApi
{
/**
* @OA\Get(
* path="/api/v1/expected-expenses",
* summary="미지급비용 목록 조회",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="search", in="query", description="검색어 (거래처명, 계정과목, 적요)", @OA\Schema(type="string")),
* @OA\Parameter(name="start_date", in="query", description="시작일", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="end_date", in="query", description="종료일", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="client_id", in="query", description="거래처 ID", @OA\Schema(type="integer")),
* @OA\Parameter(name="transaction_type", in="query", description="거래유형", @OA\Schema(type="string")),
* @OA\Parameter(name="payment_status", in="query", description="지급상태", @OA\Schema(type="string")),
* @OA\Parameter(name="approval_status", in="query", description="결재상태", @OA\Schema(type="string")),
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", default="expected_payment_date")),
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="asc")),
* @OA\Parameter(name="per_page", in="query", description="페이지당 항목 수", @OA\Schema(type="integer", default=50)),
* @OA\Parameter(name="page", in="query", description="페이지 번호", @OA\Schema(type="integer", default=1)),
* @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/ExpectedExpensePagination")
* )
* )
* )
*/
public function index() {}
/**
* @OA\Post(
* path="/api/v1/expected-expenses",
* summary="미지급비용 등록",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/ExpectedExpenseCreateRequest")
* ),
* @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/ExpectedExpense")
* )
* )
* )
*/
public function store() {}
/**
* @OA\Get(
* path="/api/v1/expected-expenses/{id}",
* summary="미지급비용 상세 조회",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="미지급비용 ID", @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/ExpectedExpense")
* )
* )
* )
*/
public function show() {}
/**
* @OA\Put(
* path="/api/v1/expected-expenses/{id}",
* summary="미지급비용 수정",
* tags={"ExpectedExpense"},
* 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/ExpectedExpenseUpdateRequest")
* ),
* @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/ExpectedExpense")
* )
* )
* )
*/
public function update() {}
/**
* @OA\Delete(
* path="/api/v1/expected-expenses/{id}",
* summary="미지급비용 삭제",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="미지급비용 ID", @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")
* )
* )
* )
*/
public function destroy() {}
/**
* @OA\Delete(
* path="/api/v1/expected-expenses",
* summary="미지급비용 일괄 삭제",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="ids", type="array", @OA\Items(type="integer"), example={1, 2, 3})
* )
* ),
* @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="object",
* @OA\Property(property="deleted_count", type="integer", example=3)
* )
* )
* )
* )
*/
public function destroyMany() {}
/**
* @OA\Put(
* path="/api/v1/expected-expenses/update-payment-date",
* summary="예상 지급일 일괄 변경",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="ids", type="array", @OA\Items(type="integer"), example={1, 2, 3}),
* @OA\Property(property="expected_payment_date", type="string", format="date", example="2025-02-01")
* )
* ),
* @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="object",
* @OA\Property(property="updated_count", type="integer", example=3)
* )
* )
* )
* )
*/
public function updateExpectedPaymentDate() {}
/**
* @OA\Get(
* path="/api/v1/expected-expenses/summary",
* summary="미지급비용 요약 (기간별 합계)",
* tags={"ExpectedExpense"},
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="start_date", in="query", description="시작일", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="end_date", in="query", description="종료일", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="payment_status", in="query", description="지급상태", @OA\Schema(type="string")),
* @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/ExpectedExpenseSummary")
* )
* )
* )
*/
public function summary() {}
}

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('expected_expenses', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->date('expected_payment_date')->comment('예상 지급일');
$table->date('settlement_date')->nullable()->comment('결제일');
$table->string('transaction_type', 30)->comment('거래유형: purchase/advance/suspense/rent/salary/insurance/tax/utilities/other');
$table->decimal('amount', 15, 2)->comment('지출금액');
$table->unsignedBigInteger('client_id')->nullable()->comment('거래처 ID');
$table->string('client_name', 100)->nullable()->comment('비회원 거래처명');
$table->unsignedBigInteger('bank_account_id')->nullable()->comment('계좌 ID');
$table->string('account_code', 50)->nullable()->comment('계정과목');
$table->string('payment_status', 20)->default('pending')->comment('지급상태: pending/partial/paid/overdue');
$table->string('approval_status', 20)->default('none')->comment('결재상태: none/pending/approved/rejected');
$table->text('description')->nullable()->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', 'expected_payment_date'], 'idx_tenant_expected_date');
$table->index(['tenant_id', 'payment_status'], 'idx_tenant_payment_status');
$table->index('client_id', 'idx_client');
$table->index('transaction_type', 'idx_transaction_type');
});
}
public function down(): void
{
Schema::dropIfExists('expected_expenses');
}
};

View File

@@ -34,6 +34,7 @@
use App\Http\Controllers\Api\V1\Design\DesignModelController;
use App\Http\Controllers\Api\V1\Design\ModelVersionController as DesignModelVersionController;
use App\Http\Controllers\Api\V1\EmployeeController;
use App\Http\Controllers\Api\V1\ExpectedExpenseController;
use App\Http\Controllers\Api\V1\EstimateController;
use App\Http\Controllers\Api\V1\FileStorageController;
use App\Http\Controllers\Api\V1\FolderController;
@@ -66,6 +67,7 @@
use App\Http\Controllers\Api\V1\PostController;
use App\Http\Controllers\Api\V1\PricingController;
use App\Http\Controllers\Api\V1\PurchaseController;
use App\Http\Controllers\Api\V1\ReceivingController;
use App\Http\Controllers\Api\V1\PushNotificationController;
use App\Http\Controllers\Api\V1\QuoteController;
use App\Http\Controllers\Api\V1\RefreshController;
@@ -89,6 +91,7 @@
use App\Http\Controllers\Api\V1\UserInvitationController;
use App\Http\Controllers\Api\V1\UserRoleController;
use App\Http\Controllers\Api\V1\WithdrawalController;
use App\Http\Controllers\Api\V1\WorkOrderController;
use App\Http\Controllers\Api\V1\WorkSettingController;
use Illuminate\Support\Facades\Route;
@@ -445,6 +448,18 @@
Route::patch('/{id}/status', [SalaryController::class, 'updateStatus'])->whereNumber('id')->name('v1.salaries.update-status');
});
// Expected Expense API (미지급비용 관리)
Route::prefix('expected-expenses')->group(function () {
Route::get('', [ExpectedExpenseController::class, 'index'])->name('v1.expected-expenses.index');
Route::post('', [ExpectedExpenseController::class, 'store'])->name('v1.expected-expenses.store');
Route::get('/summary', [ExpectedExpenseController::class, 'summary'])->name('v1.expected-expenses.summary');
Route::delete('', [ExpectedExpenseController::class, 'destroyMany'])->name('v1.expected-expenses.destroy-many');
Route::put('/update-payment-date', [ExpectedExpenseController::class, 'updateExpectedPaymentDate'])->name('v1.expected-expenses.update-payment-date');
Route::get('/{id}', [ExpectedExpenseController::class, 'show'])->whereNumber('id')->name('v1.expected-expenses.show');
Route::put('/{id}', [ExpectedExpenseController::class, 'update'])->whereNumber('id')->name('v1.expected-expenses.update');
Route::delete('/{id}', [ExpectedExpenseController::class, 'destroy'])->whereNumber('id')->name('v1.expected-expenses.destroy');
});
// Loan API (가지급금 관리)
Route::prefix('loans')->group(function () {
Route::get('', [LoanController::class, 'index'])->name('v1.loans.index');
@@ -533,6 +548,17 @@
Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm');
});
// Receiving API (입고 관리)
Route::prefix('receivings')->group(function () {
Route::get('', [ReceivingController::class, 'index'])->name('v1.receivings.index');
Route::post('', [ReceivingController::class, 'store'])->name('v1.receivings.store');
Route::get('/stats', [ReceivingController::class, 'stats'])->name('v1.receivings.stats');
Route::get('/{id}', [ReceivingController::class, 'show'])->whereNumber('id')->name('v1.receivings.show');
Route::put('/{id}', [ReceivingController::class, 'update'])->whereNumber('id')->name('v1.receivings.update');
Route::delete('/{id}', [ReceivingController::class, 'destroy'])->whereNumber('id')->name('v1.receivings.destroy');
Route::post('/{id}/process', [ReceivingController::class, 'process'])->whereNumber('id')->name('v1.receivings.process');
});
// Barobill Setting API (바로빌 설정)
Route::prefix('barobill-settings')->group(function () {
Route::get('', [BarobillSettingController::class, 'show'])->name('v1.barobill-settings.show');
@@ -926,6 +952,28 @@
Route::post('/preview/{model_set_id}', [EstimateController::class, 'previewCalculation'])->name('v1.estimates.preview'); // 견적 계산 미리보기
});
// 작업지시 관리 API (Production)
Route::prefix('work-orders')->group(function () {
// 기본 CRUD
Route::get('', [WorkOrderController::class, 'index'])->name('v1.work-orders.index'); // 목록
Route::get('/stats', [WorkOrderController::class, 'stats'])->name('v1.work-orders.stats'); // 통계
Route::post('', [WorkOrderController::class, 'store'])->name('v1.work-orders.store'); // 생성
Route::get('/{id}', [WorkOrderController::class, 'show'])->whereNumber('id')->name('v1.work-orders.show'); // 상세
Route::put('/{id}', [WorkOrderController::class, 'update'])->whereNumber('id')->name('v1.work-orders.update'); // 수정
Route::delete('/{id}', [WorkOrderController::class, 'destroy'])->whereNumber('id')->name('v1.work-orders.destroy'); // 삭제
// 상태 및 담당자 관리
Route::patch('/{id}/status', [WorkOrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.work-orders.status'); // 상태 변경
Route::patch('/{id}/assign', [WorkOrderController::class, 'assign'])->whereNumber('id')->name('v1.work-orders.assign'); // 담당자 배정
// 벤딩 공정 상세 토글
Route::patch('/{id}/bending/toggle', [WorkOrderController::class, 'toggleBendingField'])->whereNumber('id')->name('v1.work-orders.bending-toggle');
// 이슈 관리
Route::post('/{id}/issues', [WorkOrderController::class, 'addIssue'])->whereNumber('id')->name('v1.work-orders.issues.store'); // 이슈 등록
Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결
});
// 파일 저장소 API
Route::prefix('files')->group(function () {
Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시)