feat: [finance] 계정과목 및 일반전표 API 추가
- AccountCode 모델/서비스/컨트롤러 구현 - JournalEntry, JournalEntryLine 모델 구현 - GeneralJournalEntry 서비스/컨트롤러 구현 - FormRequest 검증 클래스 추가 - finance 라우트 등록 - i18n 메시지 키 추가 (message.php, error.php) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
60
app/Http/Controllers/Api/V1/AccountSubjectController.php
Normal file
60
app/Http/Controllers/Api/V1/AccountSubjectController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\AccountSubject\StoreAccountSubjectRequest;
|
||||
use App\Services\AccountCodeService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AccountSubjectController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccountCodeService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 계정과목 목록 조회
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only(['search', 'category']);
|
||||
|
||||
$subjects = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($subjects, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 등록
|
||||
*/
|
||||
public function store(StoreAccountSubjectRequest $request)
|
||||
{
|
||||
$subject = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($subject, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 활성/비활성 토글
|
||||
*/
|
||||
public function toggleStatus(int $id, Request $request)
|
||||
{
|
||||
$isActive = (bool) $request->input('is_active', true);
|
||||
|
||||
$subject = $this->service->toggleStatus($id, $isActive);
|
||||
|
||||
return ApiResponse::success($subject, __('message.toggled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\GeneralJournalEntry\StoreManualJournalRequest;
|
||||
use App\Http\Requests\V1\GeneralJournalEntry\UpdateJournalRequest;
|
||||
use App\Services\GeneralJournalEntryService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GeneralJournalEntryController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GeneralJournalEntryService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 일반전표 통합 목록 조회
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'start_date', 'end_date', 'search', 'page', 'per_page',
|
||||
]);
|
||||
|
||||
$result = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($result, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 통계
|
||||
*/
|
||||
public function summary(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'start_date', 'end_date', 'search',
|
||||
]);
|
||||
|
||||
$summary = $this->service->summary($params);
|
||||
|
||||
return ApiResponse::success($summary, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수기전표 등록
|
||||
*/
|
||||
public function store(StoreManualJournalRequest $request)
|
||||
{
|
||||
$entry = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($entry, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표 상세 조회 (분개 수정 모달용)
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$detail = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($detail, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 수정
|
||||
*/
|
||||
public function updateJournal(int $id, UpdateJournalRequest $request)
|
||||
{
|
||||
$entry = $this->service->updateJournal($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($entry, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 삭제
|
||||
*/
|
||||
public function destroyJournal(int $id)
|
||||
{
|
||||
$this->service->destroyJournal($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\AccountSubject;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreAccountSubjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => ['required', 'string', 'max:10'],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'code.required' => '계정과목 코드를 입력하세요.',
|
||||
'name.required' => '계정과목명을 입력하세요.',
|
||||
'category.in' => '유효한 분류를 선택하세요.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\GeneralJournalEntry;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreManualJournalRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'journal_date' => ['required', 'date'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'rows' => ['required', 'array', 'min:2'],
|
||||
'rows.*.side' => ['required', 'in:debit,credit'],
|
||||
'rows.*.account_subject_id' => ['required', 'string', 'max:10'],
|
||||
'rows.*.vendor_id' => ['nullable', 'integer'],
|
||||
'rows.*.debit_amount' => ['required', 'integer', 'min:0'],
|
||||
'rows.*.credit_amount' => ['required', 'integer', 'min:0'],
|
||||
'rows.*.memo' => ['nullable', 'string', 'max:300'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'journal_date.required' => '전표일자를 입력하세요.',
|
||||
'rows.required' => '분개 행을 입력하세요.',
|
||||
'rows.min' => '최소 2개 이상의 분개 행이 필요합니다.',
|
||||
'rows.*.side.required' => '차/대 구분을 선택하세요.',
|
||||
'rows.*.side.in' => '차/대 구분이 올바르지 않습니다.',
|
||||
'rows.*.account_subject_id.required' => '계정과목을 선택하세요.',
|
||||
'rows.*.debit_amount.required' => '차변 금액을 입력하세요.',
|
||||
'rows.*.credit_amount.required' => '대변 금액을 입력하세요.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\GeneralJournalEntry;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateJournalRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'journal_memo' => ['sometimes', 'nullable', 'string', 'max:1000'],
|
||||
'rows' => ['sometimes', 'array', 'min:1'],
|
||||
'rows.*.side' => ['required_with:rows', 'in:debit,credit'],
|
||||
'rows.*.account_subject_id' => ['required_with:rows', 'string', 'max:10'],
|
||||
'rows.*.vendor_id' => ['nullable', 'integer'],
|
||||
'rows.*.debit_amount' => ['required_with:rows', 'integer', 'min:0'],
|
||||
'rows.*.credit_amount' => ['required_with:rows', 'integer', 'min:0'],
|
||||
'rows.*.memo' => ['nullable', 'string', 'max:300'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'rows.*.side.required_with' => '차/대 구분을 선택하세요.',
|
||||
'rows.*.side.in' => '차/대 구분이 올바르지 않습니다.',
|
||||
'rows.*.account_subject_id.required_with' => '계정과목을 선택하세요.',
|
||||
'rows.*.debit_amount.required_with' => '차변 금액을 입력하세요.',
|
||||
'rows.*.credit_amount.required_with' => '대변 금액을 입력하세요.',
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Models/Tenants/AccountCode.php
Normal file
49
app/Models/Tenants/AccountCode.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AccountCode extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code',
|
||||
'name',
|
||||
'category',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// Categories
|
||||
public const CATEGORY_ASSET = 'asset';
|
||||
public const CATEGORY_LIABILITY = 'liability';
|
||||
public const CATEGORY_CAPITAL = 'capital';
|
||||
public const CATEGORY_REVENUE = 'revenue';
|
||||
public const CATEGORY_EXPENSE = 'expense';
|
||||
|
||||
public const CATEGORIES = [
|
||||
self::CATEGORY_ASSET => '자산',
|
||||
self::CATEGORY_LIABILITY => '부채',
|
||||
self::CATEGORY_CAPITAL => '자본',
|
||||
self::CATEGORY_REVENUE => '수익',
|
||||
self::CATEGORY_EXPENSE => '비용',
|
||||
];
|
||||
|
||||
/**
|
||||
* 활성 계정과목만 조회
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
53
app/Models/Tenants/JournalEntry.php
Normal file
53
app/Models/Tenants/JournalEntry.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class JournalEntry extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'entry_no',
|
||||
'entry_date',
|
||||
'entry_type',
|
||||
'description',
|
||||
'total_debit',
|
||||
'total_credit',
|
||||
'status',
|
||||
'source_type',
|
||||
'source_key',
|
||||
'created_by_name',
|
||||
'attachment_note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'entry_date' => 'date',
|
||||
'total_debit' => 'integer',
|
||||
'total_credit' => 'integer',
|
||||
];
|
||||
|
||||
// Status
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_CONFIRMED = 'confirmed';
|
||||
|
||||
// Source type
|
||||
public const SOURCE_MANUAL = 'manual';
|
||||
public const SOURCE_BANK_TRANSACTION = 'bank_transaction';
|
||||
|
||||
// Entry type
|
||||
public const TYPE_GENERAL = 'general';
|
||||
|
||||
/**
|
||||
* 분개 행 관계
|
||||
*/
|
||||
public function lines(): HasMany
|
||||
{
|
||||
return $this->hasMany(JournalEntryLine::class)->orderBy('line_no');
|
||||
}
|
||||
}
|
||||
45
app/Models/Tenants/JournalEntryLine.php
Normal file
45
app/Models/Tenants/JournalEntryLine.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class JournalEntryLine extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'journal_entry_id',
|
||||
'line_no',
|
||||
'dc_type',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'trading_partner_id',
|
||||
'trading_partner_name',
|
||||
'debit_amount',
|
||||
'credit_amount',
|
||||
'description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'line_no' => 'integer',
|
||||
'debit_amount' => 'integer',
|
||||
'credit_amount' => 'integer',
|
||||
'trading_partner_id' => 'integer',
|
||||
];
|
||||
|
||||
// DC Type
|
||||
public const DC_DEBIT = 'debit';
|
||||
public const DC_CREDIT = 'credit';
|
||||
|
||||
/**
|
||||
* 전표 관계
|
||||
*/
|
||||
public function journalEntry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(JournalEntry::class);
|
||||
}
|
||||
}
|
||||
109
app/Services/AccountCodeService.php
Normal file
109
app/Services/AccountCodeService.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\AccountCode;
|
||||
use App\Models\Tenants\JournalEntryLine;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class AccountCodeService extends Service
|
||||
{
|
||||
/**
|
||||
* 계정과목 목록 조회
|
||||
*/
|
||||
public function index(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = AccountCode::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색 (코드/이름)
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('code', 'like', "%{$search}%")
|
||||
->orWhere('name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 분류 필터
|
||||
if (! empty($params['category'])) {
|
||||
$query->where('category', $params['category']);
|
||||
}
|
||||
|
||||
return $query->orderBy('sort_order')->orderBy('code')->get()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 등록
|
||||
*/
|
||||
public function store(array $data): AccountCode
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 중복 코드 체크
|
||||
$exists = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['code'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new BadRequestHttpException(__('error.account_subject.duplicate_code'));
|
||||
}
|
||||
|
||||
$accountCode = new AccountCode;
|
||||
$accountCode->tenant_id = $tenantId;
|
||||
$accountCode->code = $data['code'];
|
||||
$accountCode->name = $data['name'];
|
||||
$accountCode->category = $data['category'] ?? null;
|
||||
$accountCode->sort_order = $data['sort_order'] ?? 0;
|
||||
$accountCode->is_active = true;
|
||||
$accountCode->save();
|
||||
|
||||
return $accountCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 활성/비활성 토글
|
||||
*/
|
||||
public function toggleStatus(int $id, bool $isActive): AccountCode
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$accountCode = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$accountCode->is_active = $isActive;
|
||||
$accountCode->save();
|
||||
|
||||
return $accountCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 삭제 (사용 중이면 차단)
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$accountCode = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 전표에서 사용 중인지 확인
|
||||
$inUse = JournalEntryLine::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('account_code', $accountCode->code)
|
||||
->exists();
|
||||
|
||||
if ($inUse) {
|
||||
throw new BadRequestHttpException(__('error.account_subject.in_use'));
|
||||
}
|
||||
|
||||
$accountCode->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
578
app/Services/GeneralJournalEntryService.php
Normal file
578
app/Services/GeneralJournalEntryService.php
Normal file
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\AccountCode;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Models\Tenants\JournalEntryLine;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class GeneralJournalEntryService extends Service
|
||||
{
|
||||
/**
|
||||
* 일반전표입력 통합 목록 (입금 + 출금 + 수기전표)
|
||||
* deposits/withdrawals는 계좌이체 건만, LEFT JOIN journal_entries로 분개 여부 표시
|
||||
*/
|
||||
public function index(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$startDate = $params['start_date'] ?? null;
|
||||
$endDate = $params['end_date'] ?? null;
|
||||
$search = $params['search'] ?? null;
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$page = (int) ($params['page'] ?? 1);
|
||||
|
||||
// 1) 입금(transfer) UNION 출금(transfer) UNION 수기전표
|
||||
$depositsQuery = DB::table('deposits')
|
||||
->leftJoin('journal_entries', function ($join) use ($tenantId) {
|
||||
$join->on('journal_entries.source_key', '=', DB::raw("CONCAT('deposit_', deposits.id)"))
|
||||
->where('journal_entries.tenant_id', $tenantId)
|
||||
->where('journal_entries.source_type', JournalEntry::SOURCE_BANK_TRANSACTION)
|
||||
->whereNull('journal_entries.deleted_at');
|
||||
})
|
||||
->where('deposits.tenant_id', $tenantId)
|
||||
->where('deposits.payment_method', 'transfer')
|
||||
->whereNull('deposits.deleted_at')
|
||||
->select([
|
||||
'deposits.id',
|
||||
'deposits.deposit_date as date',
|
||||
DB::raw("'deposit' as division"),
|
||||
'deposits.amount',
|
||||
'deposits.description',
|
||||
DB::raw('COALESCE(journal_entries.description, NULL) as journal_description'),
|
||||
'deposits.amount as deposit_amount',
|
||||
DB::raw('0 as withdrawal_amount'),
|
||||
DB::raw('COALESCE(journal_entries.total_debit, 0) as debit_amount'),
|
||||
DB::raw('COALESCE(journal_entries.total_credit, 0) as credit_amount'),
|
||||
DB::raw("CASE WHEN journal_entries.id IS NOT NULL THEN 'linked' ELSE 'manual' END as source"),
|
||||
'deposits.created_at',
|
||||
'deposits.updated_at',
|
||||
DB::raw('journal_entries.id as journal_entry_id'),
|
||||
]);
|
||||
|
||||
$withdrawalsQuery = DB::table('withdrawals')
|
||||
->leftJoin('journal_entries', function ($join) use ($tenantId) {
|
||||
$join->on('journal_entries.source_key', '=', DB::raw("CONCAT('withdrawal_', withdrawals.id)"))
|
||||
->where('journal_entries.tenant_id', $tenantId)
|
||||
->where('journal_entries.source_type', JournalEntry::SOURCE_BANK_TRANSACTION)
|
||||
->whereNull('journal_entries.deleted_at');
|
||||
})
|
||||
->where('withdrawals.tenant_id', $tenantId)
|
||||
->where('withdrawals.payment_method', 'transfer')
|
||||
->whereNull('withdrawals.deleted_at')
|
||||
->select([
|
||||
'withdrawals.id',
|
||||
'withdrawals.withdrawal_date as date',
|
||||
DB::raw("'withdrawal' as division"),
|
||||
'withdrawals.amount',
|
||||
'withdrawals.description',
|
||||
DB::raw('COALESCE(journal_entries.description, NULL) as journal_description'),
|
||||
DB::raw('0 as deposit_amount'),
|
||||
'withdrawals.amount as withdrawal_amount',
|
||||
DB::raw('COALESCE(journal_entries.total_debit, 0) as debit_amount'),
|
||||
DB::raw('COALESCE(journal_entries.total_credit, 0) as credit_amount'),
|
||||
DB::raw("CASE WHEN journal_entries.id IS NOT NULL THEN 'linked' ELSE 'manual' END as source"),
|
||||
'withdrawals.created_at',
|
||||
'withdrawals.updated_at',
|
||||
DB::raw('journal_entries.id as journal_entry_id'),
|
||||
]);
|
||||
|
||||
$manualQuery = DB::table('journal_entries')
|
||||
->where('journal_entries.tenant_id', $tenantId)
|
||||
->where('journal_entries.source_type', JournalEntry::SOURCE_MANUAL)
|
||||
->whereNull('journal_entries.deleted_at')
|
||||
->select([
|
||||
'journal_entries.id',
|
||||
'journal_entries.entry_date as date',
|
||||
DB::raw("'transfer' as division"),
|
||||
'journal_entries.total_debit as amount',
|
||||
'journal_entries.description',
|
||||
'journal_entries.description as journal_description',
|
||||
DB::raw('0 as deposit_amount'),
|
||||
DB::raw('0 as withdrawal_amount'),
|
||||
'journal_entries.total_debit as debit_amount',
|
||||
'journal_entries.total_credit as credit_amount',
|
||||
DB::raw("'manual' as source"),
|
||||
'journal_entries.created_at',
|
||||
'journal_entries.updated_at',
|
||||
DB::raw('journal_entries.id as journal_entry_id'),
|
||||
]);
|
||||
|
||||
// 날짜 필터
|
||||
if ($startDate) {
|
||||
$depositsQuery->where('deposits.deposit_date', '>=', $startDate);
|
||||
$withdrawalsQuery->where('withdrawals.withdrawal_date', '>=', $startDate);
|
||||
$manualQuery->where('journal_entries.entry_date', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$depositsQuery->where('deposits.deposit_date', '<=', $endDate);
|
||||
$withdrawalsQuery->where('withdrawals.withdrawal_date', '<=', $endDate);
|
||||
$manualQuery->where('journal_entries.entry_date', '<=', $endDate);
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if ($search) {
|
||||
$depositsQuery->where(function ($q) use ($search) {
|
||||
$q->where('deposits.description', 'like', "%{$search}%")
|
||||
->orWhere('deposits.client_name', 'like', "%{$search}%");
|
||||
});
|
||||
$withdrawalsQuery->where(function ($q) use ($search) {
|
||||
$q->where('withdrawals.description', 'like', "%{$search}%")
|
||||
->orWhere('withdrawals.client_name', 'like', "%{$search}%");
|
||||
});
|
||||
$manualQuery->where('journal_entries.description', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
// UNION
|
||||
$unionQuery = $depositsQuery
|
||||
->unionAll($withdrawalsQuery)
|
||||
->unionAll($manualQuery);
|
||||
|
||||
// 전체 건수
|
||||
$totalCount = DB::table(DB::raw("({$unionQuery->toSql()}) as union_table"))
|
||||
->mergeBindings($unionQuery)
|
||||
->count();
|
||||
|
||||
// 날짜순 정렬 + 페이지네이션
|
||||
$items = DB::table(DB::raw("({$unionQuery->toSql()}) as union_table"))
|
||||
->mergeBindings($unionQuery)
|
||||
->orderBy('date', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->offset(($page - 1) * $perPage)
|
||||
->limit($perPage)
|
||||
->get();
|
||||
|
||||
// 누적잔액 계산 (해당 기간 전체 기준)
|
||||
$allForBalance = DB::table(DB::raw("({$unionQuery->toSql()}) as union_table"))
|
||||
->mergeBindings($unionQuery)
|
||||
->orderBy('date', 'asc')
|
||||
->orderBy('created_at', 'asc')
|
||||
->get(['deposit_amount', 'withdrawal_amount']);
|
||||
|
||||
$runningBalance = 0;
|
||||
$balanceMap = [];
|
||||
foreach ($allForBalance as $idx => $row) {
|
||||
$runningBalance += (int) $row->deposit_amount - (int) $row->withdrawal_amount;
|
||||
$balanceMap[$idx] = $runningBalance;
|
||||
}
|
||||
|
||||
// 역순이므로 현재 페이지에 해당하는 잔액을 매핑
|
||||
$totalItems = count($allForBalance);
|
||||
$items = $items->map(function ($item, $index) use ($balanceMap, $totalItems, $page, $perPage) {
|
||||
// 역순 인덱스 → 정순 인덱스
|
||||
$reverseIdx = $totalItems - 1 - (($page - 1) * $perPage + $index);
|
||||
$item->balance = $reverseIdx >= 0 ? ($balanceMap[$reverseIdx] ?? 0) : 0;
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return [
|
||||
'data' => $items->toArray(),
|
||||
'meta' => [
|
||||
'current_page' => $page,
|
||||
'last_page' => (int) ceil($totalCount / $perPage),
|
||||
'per_page' => $perPage,
|
||||
'total' => $totalCount,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 통계
|
||||
*/
|
||||
public function summary(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$startDate = $params['start_date'] ?? null;
|
||||
$endDate = $params['end_date'] ?? null;
|
||||
$search = $params['search'] ?? null;
|
||||
|
||||
// 입금 통계
|
||||
$depositQuery = DB::table('deposits')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('payment_method', 'transfer')
|
||||
->whereNull('deleted_at');
|
||||
|
||||
// 출금 통계
|
||||
$withdrawalQuery = DB::table('withdrawals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('payment_method', 'transfer')
|
||||
->whereNull('deleted_at');
|
||||
|
||||
if ($startDate) {
|
||||
$depositQuery->where('deposit_date', '>=', $startDate);
|
||||
$withdrawalQuery->where('withdrawal_date', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$depositQuery->where('deposit_date', '<=', $endDate);
|
||||
$withdrawalQuery->where('withdrawal_date', '<=', $endDate);
|
||||
}
|
||||
if ($search) {
|
||||
$depositQuery->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhere('client_name', 'like', "%{$search}%");
|
||||
});
|
||||
$withdrawalQuery->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhere('client_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$depositCount = (clone $depositQuery)->count();
|
||||
$depositAmount = (int) (clone $depositQuery)->sum('amount');
|
||||
$withdrawalCount = (clone $withdrawalQuery)->count();
|
||||
$withdrawalAmount = (int) (clone $withdrawalQuery)->sum('amount');
|
||||
|
||||
// 분개 완료/미완료 건수 (journal_entries가 연결된 입출금 수)
|
||||
$journalCompleteCount = DB::table('journal_entries')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_type', JournalEntry::SOURCE_BANK_TRANSACTION)
|
||||
->whereNull('deleted_at')
|
||||
->when($startDate, fn ($q) => $q->where('entry_date', '>=', $startDate))
|
||||
->when($endDate, fn ($q) => $q->where('entry_date', '<=', $endDate))
|
||||
->count();
|
||||
|
||||
$totalCount = $depositCount + $withdrawalCount;
|
||||
$journalIncompleteCount = max(0, $totalCount - $journalCompleteCount);
|
||||
|
||||
return [
|
||||
'total_count' => $totalCount,
|
||||
'deposit_count' => $depositCount,
|
||||
'deposit_amount' => $depositAmount,
|
||||
'withdrawal_count' => $withdrawalCount,
|
||||
'withdrawal_amount' => $withdrawalAmount,
|
||||
'journal_complete_count' => $journalCompleteCount,
|
||||
'journal_incomplete_count' => $journalIncompleteCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표 상세 조회 (분개 수정 모달용)
|
||||
*/
|
||||
public function show(int $id): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$entry = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('lines')
|
||||
->findOrFail($id);
|
||||
|
||||
// source_type에 따라 원본 거래 정보 조회
|
||||
$sourceInfo = $this->getSourceInfo($entry);
|
||||
|
||||
return [
|
||||
'id' => $entry->id,
|
||||
'date' => $entry->entry_date->format('Y-m-d'),
|
||||
'division' => $sourceInfo['division'],
|
||||
'amount' => $sourceInfo['amount'],
|
||||
'description' => $sourceInfo['description'] ?? $entry->description,
|
||||
'bank_name' => $sourceInfo['bank_name'] ?? '',
|
||||
'account_number' => $sourceInfo['account_number'] ?? '',
|
||||
'journal_memo' => $entry->description,
|
||||
'rows' => $entry->lines->map(function ($line) {
|
||||
return [
|
||||
'id' => $line->id,
|
||||
'side' => $line->dc_type,
|
||||
'account_subject_id' => $line->account_code,
|
||||
'account_subject_name' => $line->account_name,
|
||||
'vendor_id' => $line->trading_partner_id,
|
||||
'vendor_name' => $line->trading_partner_name ?? '',
|
||||
'debit_amount' => (int) $line->debit_amount,
|
||||
'credit_amount' => (int) $line->credit_amount,
|
||||
'memo' => $line->description ?? '',
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 수기전표 등록
|
||||
*/
|
||||
public function store(array $data): JournalEntry
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId) {
|
||||
// 차대 균형 검증
|
||||
$this->validateDebitCreditBalance($data['rows']);
|
||||
|
||||
// 전표번호 생성
|
||||
$entryNo = $this->generateEntryNo($tenantId, $data['journal_date']);
|
||||
|
||||
// 합계 계산
|
||||
$totalDebit = 0;
|
||||
$totalCredit = 0;
|
||||
foreach ($data['rows'] as $row) {
|
||||
$totalDebit += (int) ($row['debit_amount'] ?? 0);
|
||||
$totalCredit += (int) ($row['credit_amount'] ?? 0);
|
||||
}
|
||||
|
||||
// 전표 생성
|
||||
$entry = new JournalEntry;
|
||||
$entry->tenant_id = $tenantId;
|
||||
$entry->entry_no = $entryNo;
|
||||
$entry->entry_date = $data['journal_date'];
|
||||
$entry->entry_type = JournalEntry::TYPE_GENERAL;
|
||||
$entry->description = $data['description'] ?? null;
|
||||
$entry->total_debit = $totalDebit;
|
||||
$entry->total_credit = $totalCredit;
|
||||
$entry->status = JournalEntry::STATUS_CONFIRMED;
|
||||
$entry->source_type = JournalEntry::SOURCE_MANUAL;
|
||||
$entry->source_key = null;
|
||||
$entry->save();
|
||||
|
||||
// 분개 행 생성
|
||||
$this->createLines($entry, $data['rows'], $tenantId);
|
||||
|
||||
return $entry->load('lines');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 수정 (lines 전체 교체)
|
||||
*/
|
||||
public function updateJournal(int $id, array $data): JournalEntry
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId) {
|
||||
$entry = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 메모 업데이트
|
||||
if (array_key_exists('journal_memo', $data)) {
|
||||
$entry->description = $data['journal_memo'];
|
||||
}
|
||||
|
||||
// rows가 있으면 lines 교체
|
||||
if (isset($data['rows']) && ! empty($data['rows'])) {
|
||||
$this->validateDebitCreditBalance($data['rows']);
|
||||
|
||||
// 기존 lines 삭제
|
||||
JournalEntryLine::query()
|
||||
->where('journal_entry_id', $entry->id)
|
||||
->delete();
|
||||
|
||||
// 새 lines 생성
|
||||
$this->createLines($entry, $data['rows'], $tenantId);
|
||||
|
||||
// 합계 재계산
|
||||
$totalDebit = 0;
|
||||
$totalCredit = 0;
|
||||
foreach ($data['rows'] as $row) {
|
||||
$totalDebit += (int) ($row['debit_amount'] ?? 0);
|
||||
$totalCredit += (int) ($row['credit_amount'] ?? 0);
|
||||
}
|
||||
|
||||
$entry->total_debit = $totalDebit;
|
||||
$entry->total_credit = $totalCredit;
|
||||
}
|
||||
|
||||
$entry->save();
|
||||
|
||||
return $entry->load('lines');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표 삭제 (soft delete, lines는 FK CASCADE)
|
||||
*/
|
||||
public function destroyJournal(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId) {
|
||||
$entry = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// lines 먼저 삭제 (soft delete가 아니므로 물리 삭제)
|
||||
JournalEntryLine::query()
|
||||
->where('journal_entry_id', $entry->id)
|
||||
->delete();
|
||||
|
||||
$entry->delete(); // soft delete
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표번호 생성: JE-YYYYMMDD-NNN (동시성 안전)
|
||||
*/
|
||||
private function generateEntryNo(int $tenantId, string $date): string
|
||||
{
|
||||
$dateStr = str_replace('-', '', substr($date, 0, 10));
|
||||
$prefix = "JE-{$dateStr}-";
|
||||
|
||||
// SELECT ... FOR UPDATE 락으로 동시성 안전 보장
|
||||
$lastEntry = DB::table('journal_entries')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('entry_no', 'like', "{$prefix}%")
|
||||
->lockForUpdate()
|
||||
->orderBy('entry_no', 'desc')
|
||||
->first(['entry_no']);
|
||||
|
||||
if ($lastEntry) {
|
||||
$lastSeq = (int) substr($lastEntry->entry_no, -3);
|
||||
$nextSeq = $lastSeq + 1;
|
||||
} else {
|
||||
$nextSeq = 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($nextSeq, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차대 균형 검증
|
||||
*/
|
||||
private function validateDebitCreditBalance(array $rows): void
|
||||
{
|
||||
$totalDebit = 0;
|
||||
$totalCredit = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$totalDebit += (int) ($row['debit_amount'] ?? 0);
|
||||
$totalCredit += (int) ($row['credit_amount'] ?? 0);
|
||||
}
|
||||
|
||||
if ($totalDebit !== $totalCredit) {
|
||||
throw new BadRequestHttpException(__('error.journal_entry.debit_credit_mismatch'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 행 생성
|
||||
*/
|
||||
private function createLines(JournalEntry $entry, array $rows, int $tenantId): void
|
||||
{
|
||||
foreach ($rows as $index => $row) {
|
||||
$accountCode = $row['account_subject_id'] ?? '';
|
||||
$accountName = $this->resolveAccountName($tenantId, $accountCode);
|
||||
$vendorName = $this->resolveVendorName($row['vendor_id'] ?? null);
|
||||
|
||||
$line = new JournalEntryLine;
|
||||
$line->tenant_id = $tenantId;
|
||||
$line->journal_entry_id = $entry->id;
|
||||
$line->line_no = $index + 1;
|
||||
$line->dc_type = $row['side'];
|
||||
$line->account_code = $accountCode;
|
||||
$line->account_name = $accountName;
|
||||
$line->trading_partner_id = ! empty($row['vendor_id']) ? (int) $row['vendor_id'] : null;
|
||||
$line->trading_partner_name = $vendorName;
|
||||
$line->debit_amount = (int) ($row['debit_amount'] ?? 0);
|
||||
$line->credit_amount = (int) ($row['credit_amount'] ?? 0);
|
||||
$line->description = $row['memo'] ?? null;
|
||||
$line->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 코드 → 이름 조회
|
||||
*/
|
||||
private function resolveAccountName(int $tenantId, string $code): string
|
||||
{
|
||||
if (empty($code)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$account = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $code)
|
||||
->first(['name']);
|
||||
|
||||
return $account ? $account->name : $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 ID → 이름 조회
|
||||
*/
|
||||
private function resolveVendorName(?int $vendorId): string
|
||||
{
|
||||
if (! $vendorId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$vendor = DB::table('clients')
|
||||
->where('id', $vendorId)
|
||||
->first(['name']);
|
||||
|
||||
return $vendor ? $vendor->name : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 원본 거래 정보 조회 (입금/출금)
|
||||
*/
|
||||
private function getSourceInfo(JournalEntry $entry): array
|
||||
{
|
||||
if ($entry->source_type === JournalEntry::SOURCE_MANUAL) {
|
||||
return [
|
||||
'division' => 'transfer',
|
||||
'amount' => $entry->total_debit,
|
||||
'description' => $entry->description,
|
||||
'bank_name' => '',
|
||||
'account_number' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// bank_transaction → deposit_123 / withdrawal_456
|
||||
if ($entry->source_key && str_starts_with($entry->source_key, 'deposit_')) {
|
||||
$sourceId = (int) str_replace('deposit_', '', $entry->source_key);
|
||||
$deposit = DB::table('deposits')
|
||||
->leftJoin('bank_accounts', 'deposits.bank_account_id', '=', 'bank_accounts.id')
|
||||
->where('deposits.id', $sourceId)
|
||||
->first([
|
||||
'deposits.amount',
|
||||
'deposits.description',
|
||||
'bank_accounts.bank_name',
|
||||
'bank_accounts.account_number',
|
||||
]);
|
||||
|
||||
if ($deposit) {
|
||||
return [
|
||||
'division' => 'deposit',
|
||||
'amount' => (int) $deposit->amount,
|
||||
'description' => $deposit->description,
|
||||
'bank_name' => $deposit->bank_name ?? '',
|
||||
'account_number' => $deposit->account_number ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($entry->source_key && str_starts_with($entry->source_key, 'withdrawal_')) {
|
||||
$sourceId = (int) str_replace('withdrawal_', '', $entry->source_key);
|
||||
$withdrawal = DB::table('withdrawals')
|
||||
->leftJoin('bank_accounts', 'withdrawals.bank_account_id', '=', 'bank_accounts.id')
|
||||
->where('withdrawals.id', $sourceId)
|
||||
->first([
|
||||
'withdrawals.amount',
|
||||
'withdrawals.description',
|
||||
'bank_accounts.bank_name',
|
||||
'bank_accounts.account_number',
|
||||
]);
|
||||
|
||||
if ($withdrawal) {
|
||||
return [
|
||||
'division' => 'withdrawal',
|
||||
'amount' => (int) $withdrawal->amount,
|
||||
'description' => $withdrawal->description,
|
||||
'bank_name' => $withdrawal->bank_name ?? '',
|
||||
'account_number' => $withdrawal->account_number ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'division' => 'transfer',
|
||||
'amount' => $entry->total_debit,
|
||||
'description' => $entry->description,
|
||||
'bank_name' => '',
|
||||
'account_number' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -481,4 +481,15 @@
|
||||
'cannot_delete' => '해당 계약은 삭제할 수 없습니다.',
|
||||
'invalid_status' => '유효하지 않은 계약 상태입니다.',
|
||||
],
|
||||
|
||||
// 일반전표입력
|
||||
'journal_entry' => [
|
||||
'debit_credit_mismatch' => '차변 합계와 대변 합계가 일치해야 합니다.',
|
||||
],
|
||||
|
||||
// 계정과목
|
||||
'account_subject' => [
|
||||
'duplicate_code' => '이미 존재하는 계정과목 코드입니다.',
|
||||
'in_use' => '전표에서 사용 중인 계정과목은 삭제할 수 없습니다.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -567,6 +567,22 @@
|
||||
'downloaded' => '문서가 다운로드되었습니다.',
|
||||
],
|
||||
|
||||
// 일반전표입력
|
||||
'journal_entry' => [
|
||||
'fetched' => '전표 조회 성공',
|
||||
'created' => '전표가 등록되었습니다.',
|
||||
'updated' => '분개가 수정되었습니다.',
|
||||
'deleted' => '분개가 삭제되었습니다.',
|
||||
],
|
||||
|
||||
// 계정과목
|
||||
'account_subject' => [
|
||||
'fetched' => '계정과목 조회 성공',
|
||||
'created' => '계정과목이 등록되었습니다.',
|
||||
'toggled' => '계정과목 상태가 변경되었습니다.',
|
||||
'deleted' => '계정과목이 삭제되었습니다.',
|
||||
],
|
||||
|
||||
// CEO 대시보드 부가세 현황
|
||||
'vat' => [
|
||||
'sales_tax' => '매출세액',
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* - 대시보드/보고서
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Api\V1\AccountSubjectController;
|
||||
use App\Http\Controllers\Api\V1\BadDebtController;
|
||||
use App\Http\Controllers\Api\V1\BankAccountController;
|
||||
use App\Http\Controllers\Api\V1\BankTransactionController;
|
||||
@@ -26,6 +27,7 @@
|
||||
use App\Http\Controllers\Api\V1\DepositController;
|
||||
use App\Http\Controllers\Api\V1\EntertainmentController;
|
||||
use App\Http\Controllers\Api\V1\ExpectedExpenseController;
|
||||
use App\Http\Controllers\Api\V1\GeneralJournalEntryController;
|
||||
use App\Http\Controllers\Api\V1\LoanController;
|
||||
use App\Http\Controllers\Api\V1\PaymentController;
|
||||
use App\Http\Controllers\Api\V1\PayrollController;
|
||||
@@ -304,6 +306,24 @@
|
||||
Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy');
|
||||
});
|
||||
|
||||
// General Journal Entry API (일반전표입력)
|
||||
Route::prefix('general-journal-entries')->group(function () {
|
||||
Route::get('', [GeneralJournalEntryController::class, 'index'])->name('v1.general-journal-entries.index');
|
||||
Route::get('/summary', [GeneralJournalEntryController::class, 'summary'])->name('v1.general-journal-entries.summary');
|
||||
Route::post('', [GeneralJournalEntryController::class, 'store'])->name('v1.general-journal-entries.store');
|
||||
Route::get('/{id}', [GeneralJournalEntryController::class, 'show'])->whereNumber('id')->name('v1.general-journal-entries.show');
|
||||
Route::put('/{id}/journal', [GeneralJournalEntryController::class, 'updateJournal'])->whereNumber('id')->name('v1.general-journal-entries.update-journal');
|
||||
Route::delete('/{id}/journal', [GeneralJournalEntryController::class, 'destroyJournal'])->whereNumber('id')->name('v1.general-journal-entries.destroy-journal');
|
||||
});
|
||||
|
||||
// Account Subject API (계정과목)
|
||||
Route::prefix('account-subjects')->group(function () {
|
||||
Route::get('', [AccountSubjectController::class, 'index'])->name('v1.account-subjects.index');
|
||||
Route::post('', [AccountSubjectController::class, 'store'])->name('v1.account-subjects.store');
|
||||
Route::patch('/{id}/status', [AccountSubjectController::class, 'toggleStatus'])->whereNumber('id')->name('v1.account-subjects.toggle-status');
|
||||
Route::delete('/{id}', [AccountSubjectController::class, 'destroy'])->whereNumber('id')->name('v1.account-subjects.destroy');
|
||||
});
|
||||
|
||||
// Bill API (어음관리)
|
||||
Route::prefix('bills')->group(function () {
|
||||
Route::get('', [BillController::class, 'index'])->name('v1.bills.index');
|
||||
|
||||
Reference in New Issue
Block a user