feat:계정과목 설정 기능 추가 (더존 표준 계정과목)
- account_codes 테이블 및 모델 생성 - 더존 표준 계정과목 163개 시더 추가 - 계정과목 CRUD API 추가 (추가/수정/삭제/조회) - 계정과목 설정 모달 UI 구현 - 분류별 필터링 및 검색 기능 - 사용/미사용 토글 기능 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Barobill;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\AccountCode;
|
||||
use App\Models\Barobill\BarobillConfig;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Models\Barobill\BankTransaction;
|
||||
@@ -535,42 +536,30 @@ private function getBankName(string $code): string
|
||||
*/
|
||||
public function accountCodes(): JsonResponse
|
||||
{
|
||||
// 자주 사용되는 계정과목 목록
|
||||
$codes = [
|
||||
['code' => '101', 'name' => '현금'],
|
||||
['code' => '103', 'name' => '보통예금'],
|
||||
['code' => '108', 'name' => '외상매출금'],
|
||||
['code' => '110', 'name' => '받을어음'],
|
||||
['code' => '253', 'name' => '외상매입금'],
|
||||
['code' => '255', 'name' => '지급어음'],
|
||||
['code' => '401', 'name' => '매출'],
|
||||
['code' => '501', 'name' => '매입'],
|
||||
['code' => '511', 'name' => '급여'],
|
||||
['code' => '521', 'name' => '복리후생비'],
|
||||
['code' => '522', 'name' => '여비교통비'],
|
||||
['code' => '523', 'name' => '접대비'],
|
||||
['code' => '524', 'name' => '통신비'],
|
||||
['code' => '525', 'name' => '수도광열비'],
|
||||
['code' => '526', 'name' => '세금과공과'],
|
||||
['code' => '527', 'name' => '임차료'],
|
||||
['code' => '528', 'name' => '수선비'],
|
||||
['code' => '529', 'name' => '보험료'],
|
||||
['code' => '530', 'name' => '차량유지비'],
|
||||
['code' => '531', 'name' => '운반비'],
|
||||
['code' => '532', 'name' => '교육훈련비'],
|
||||
['code' => '533', 'name' => '도서인쇄비'],
|
||||
['code' => '534', 'name' => '사무용품비'],
|
||||
['code' => '535', 'name' => '소모품비'],
|
||||
['code' => '536', 'name' => '지급수수료'],
|
||||
['code' => '537', 'name' => '광고선전비'],
|
||||
['code' => '538', 'name' => '대손상각비'],
|
||||
['code' => '539', 'name' => '감가상각비'],
|
||||
['code' => '540', 'name' => '잡비'],
|
||||
['code' => '901', 'name' => '이자수익'],
|
||||
['code' => '902', 'name' => '이자비용'],
|
||||
['code' => '910', 'name' => '잡이익'],
|
||||
['code' => '920', 'name' => '잡손실'],
|
||||
];
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
$codes = AccountCode::getActiveByTenant($tenantId);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $codes->map(fn($c) => [
|
||||
'id' => $c->id,
|
||||
'code' => $c->code,
|
||||
'name' => $c->name,
|
||||
'category' => $c->category,
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 계정과목 목록 조회 (설정용, 비활성 포함)
|
||||
*/
|
||||
public function accountCodesAll(): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
$codes = AccountCode::where('tenant_id', $tenantId)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('code')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -578,6 +567,145 @@ public function accountCodes(): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 추가
|
||||
*/
|
||||
public function accountCodeStore(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'category' => 'nullable|string|max:50',
|
||||
]);
|
||||
|
||||
// 중복 체크
|
||||
$exists = AccountCode::where('tenant_id', $tenantId)
|
||||
->where('code', $validated['code'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '이미 존재하는 계정과목 코드입니다.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
$maxSort = AccountCode::where('tenant_id', $tenantId)->max('sort_order') ?? 0;
|
||||
|
||||
$accountCode = AccountCode::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'code' => $validated['code'],
|
||||
'name' => $validated['name'],
|
||||
'category' => $validated['category'] ?? null,
|
||||
'sort_order' => $maxSort + 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '계정과목이 추가되었습니다.',
|
||||
'data' => $accountCode
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '추가 실패: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 수정
|
||||
*/
|
||||
public function accountCodeUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$accountCode = AccountCode::where('tenant_id', $tenantId)
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$accountCode) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '계정과목을 찾을 수 없습니다.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'code' => 'sometimes|string|max:10',
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'category' => 'nullable|string|max:50',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
// 코드 변경 시 중복 체크
|
||||
if (isset($validated['code']) && $validated['code'] !== $accountCode->code) {
|
||||
$exists = AccountCode::where('tenant_id', $tenantId)
|
||||
->where('code', $validated['code'])
|
||||
->where('id', '!=', $id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '이미 존재하는 계정과목 코드입니다.'
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$accountCode->update($validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '계정과목이 수정되었습니다.',
|
||||
'data' => $accountCode
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '수정 실패: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 삭제
|
||||
*/
|
||||
public function accountCodeDestroy(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$accountCode = AccountCode::where('tenant_id', $tenantId)
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$accountCode) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '계정과목을 찾을 수 없습니다.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$accountCode->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '계정과목이 삭제되었습니다.'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '삭제 실패: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 입출금 내역 저장 (계정과목 포함)
|
||||
*/
|
||||
|
||||
49
app/Models/Barobill/AccountCode.php
Normal file
49
app/Models/Barobill/AccountCode.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\Tenants\Tenant;
|
||||
|
||||
/**
|
||||
* 계정과목 모델
|
||||
*/
|
||||
class AccountCode extends Model
|
||||
{
|
||||
protected $table = 'account_codes';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code',
|
||||
'name',
|
||||
'category',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 테넌트 관계
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트별 활성 계정과목 조회
|
||||
*/
|
||||
public static function getActiveByTenant(int $tenantId)
|
||||
{
|
||||
return self::where('tenant_id', $tenantId)
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('code')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('account_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
||||
$table->string('code', 10)->comment('계정과목 코드');
|
||||
$table->string('name', 100)->comment('계정과목 명');
|
||||
$table->string('category', 50)->nullable()->comment('분류 (자산/부채/자본/수익/비용)');
|
||||
$table->integer('sort_order')->default(0)->comment('정렬순서');
|
||||
$table->boolean('is_active')->default(true)->comment('사용여부');
|
||||
$table->timestamps();
|
||||
|
||||
// 테넌트별 계정과목 코드 유니크
|
||||
$table->unique(['tenant_id', 'code'], 'account_codes_tenant_code_unique');
|
||||
$table->index(['tenant_id', 'is_active'], 'account_codes_tenant_active_idx');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('account_codes');
|
||||
}
|
||||
};
|
||||
242
database/seeders/AccountCodeSeeder.php
Normal file
242
database/seeders/AccountCodeSeeder.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\Barobill\AccountCode;
|
||||
|
||||
/**
|
||||
* 더존 표준 계정과목 시더
|
||||
*/
|
||||
class AccountCodeSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1; // 기본 테넌트
|
||||
|
||||
// 기존 데이터 삭제
|
||||
AccountCode::where('tenant_id', $tenantId)->delete();
|
||||
|
||||
$accountCodes = $this->getDouzonAccountCodes();
|
||||
|
||||
$sortOrder = 0;
|
||||
foreach ($accountCodes as $code) {
|
||||
AccountCode::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'code' => $code['code'],
|
||||
'name' => $code['name'],
|
||||
'category' => $code['category'],
|
||||
'sort_order' => $sortOrder++,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info("계정과목 " . count($accountCodes) . "개 생성 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* 더존 표준 계정과목 목록
|
||||
*/
|
||||
private function getDouzonAccountCodes(): array
|
||||
{
|
||||
return [
|
||||
// ===== 자산 (1xx) =====
|
||||
// 유동자산 - 당좌자산
|
||||
['code' => '101', 'name' => '현금', 'category' => '자산'],
|
||||
['code' => '102', 'name' => '당좌예금', 'category' => '자산'],
|
||||
['code' => '103', 'name' => '보통예금', 'category' => '자산'],
|
||||
['code' => '104', 'name' => '정기예금', 'category' => '자산'],
|
||||
['code' => '105', 'name' => '정기적금', 'category' => '자산'],
|
||||
['code' => '106', 'name' => '외화예금', 'category' => '자산'],
|
||||
['code' => '107', 'name' => '별단예금', 'category' => '자산'],
|
||||
['code' => '108', 'name' => '외상매출금', 'category' => '자산'],
|
||||
['code' => '109', 'name' => '대손충당금', 'category' => '자산'],
|
||||
['code' => '110', 'name' => '받을어음', 'category' => '자산'],
|
||||
['code' => '111', 'name' => '단기대여금', 'category' => '자산'],
|
||||
['code' => '112', 'name' => '미수금', 'category' => '자산'],
|
||||
['code' => '113', 'name' => '미수수익', 'category' => '자산'],
|
||||
['code' => '114', 'name' => '선급금', 'category' => '자산'],
|
||||
['code' => '115', 'name' => '선급비용', 'category' => '자산'],
|
||||
['code' => '116', 'name' => '가지급금', 'category' => '자산'],
|
||||
['code' => '117', 'name' => '부가세대급금', 'category' => '자산'],
|
||||
['code' => '118', 'name' => '선납세금', 'category' => '자산'],
|
||||
['code' => '119', 'name' => '유가증권', 'category' => '자산'],
|
||||
['code' => '120', 'name' => '단기금융상품', 'category' => '자산'],
|
||||
|
||||
// 유동자산 - 재고자산
|
||||
['code' => '141', 'name' => '상품', 'category' => '자산'],
|
||||
['code' => '142', 'name' => '제품', 'category' => '자산'],
|
||||
['code' => '143', 'name' => '반제품', 'category' => '자산'],
|
||||
['code' => '144', 'name' => '재공품', 'category' => '자산'],
|
||||
['code' => '145', 'name' => '원재료', 'category' => '자산'],
|
||||
['code' => '146', 'name' => '부재료', 'category' => '자산'],
|
||||
['code' => '147', 'name' => '저장품', 'category' => '자산'],
|
||||
['code' => '148', 'name' => '미착품', 'category' => '자산'],
|
||||
|
||||
// 비유동자산 - 투자자산
|
||||
['code' => '151', 'name' => '장기금융상품', 'category' => '자산'],
|
||||
['code' => '152', 'name' => '장기대여금', 'category' => '자산'],
|
||||
['code' => '153', 'name' => '투자유가증권', 'category' => '자산'],
|
||||
['code' => '154', 'name' => '출자금', 'category' => '자산'],
|
||||
['code' => '155', 'name' => '장기성매출채권', 'category' => '자산'],
|
||||
['code' => '156', 'name' => '보증금', 'category' => '자산'],
|
||||
['code' => '157', 'name' => '임차보증금', 'category' => '자산'],
|
||||
|
||||
// 비유동자산 - 유형자산
|
||||
['code' => '161', 'name' => '토지', 'category' => '자산'],
|
||||
['code' => '162', 'name' => '건물', 'category' => '자산'],
|
||||
['code' => '163', 'name' => '건물감가상각누계액', 'category' => '자산'],
|
||||
['code' => '164', 'name' => '구축물', 'category' => '자산'],
|
||||
['code' => '165', 'name' => '구축물감가상각누계액', 'category' => '자산'],
|
||||
['code' => '166', 'name' => '기계장치', 'category' => '자산'],
|
||||
['code' => '167', 'name' => '기계장치감가상각누계액', 'category' => '자산'],
|
||||
['code' => '168', 'name' => '차량운반구', 'category' => '자산'],
|
||||
['code' => '169', 'name' => '차량운반구감가상각누계액', 'category' => '자산'],
|
||||
['code' => '170', 'name' => '공구와기구', 'category' => '자산'],
|
||||
['code' => '171', 'name' => '공구와기구감가상각누계액', 'category' => '자산'],
|
||||
['code' => '172', 'name' => '비품', 'category' => '자산'],
|
||||
['code' => '173', 'name' => '비품감가상각누계액', 'category' => '자산'],
|
||||
['code' => '174', 'name' => '건설중인자산', 'category' => '자산'],
|
||||
|
||||
// 비유동자산 - 무형자산
|
||||
['code' => '181', 'name' => '영업권', 'category' => '자산'],
|
||||
['code' => '182', 'name' => '산업재산권', 'category' => '자산'],
|
||||
['code' => '183', 'name' => '개발비', 'category' => '자산'],
|
||||
['code' => '184', 'name' => '소프트웨어', 'category' => '자산'],
|
||||
['code' => '185', 'name' => '창업비', 'category' => '자산'],
|
||||
|
||||
// 기타 비유동자산
|
||||
['code' => '191', 'name' => '이연법인세자산', 'category' => '자산'],
|
||||
|
||||
// ===== 부채 (2xx) =====
|
||||
// 유동부채
|
||||
['code' => '201', 'name' => '외상매입금', 'category' => '부채'],
|
||||
['code' => '202', 'name' => '지급어음', 'category' => '부채'],
|
||||
['code' => '203', 'name' => '단기차입금', 'category' => '부채'],
|
||||
['code' => '204', 'name' => '미지급금', 'category' => '부채'],
|
||||
['code' => '205', 'name' => '미지급비용', 'category' => '부채'],
|
||||
['code' => '206', 'name' => '선수금', 'category' => '부채'],
|
||||
['code' => '207', 'name' => '예수금', 'category' => '부채'],
|
||||
['code' => '208', 'name' => '부가세예수금', 'category' => '부채'],
|
||||
['code' => '209', 'name' => '미지급세금', 'category' => '부채'],
|
||||
['code' => '210', 'name' => '미지급법인세', 'category' => '부채'],
|
||||
['code' => '211', 'name' => '미지급배당금', 'category' => '부채'],
|
||||
['code' => '212', 'name' => '가수금', 'category' => '부채'],
|
||||
['code' => '213', 'name' => '선수수익', 'category' => '부채'],
|
||||
['code' => '214', 'name' => '유동성장기부채', 'category' => '부채'],
|
||||
['code' => '215', 'name' => '당기법인세부채', 'category' => '부채'],
|
||||
|
||||
// 비유동부채
|
||||
['code' => '251', 'name' => '장기차입금', 'category' => '부채'],
|
||||
['code' => '252', 'name' => '사채', 'category' => '부채'],
|
||||
['code' => '253', 'name' => '장기성매입채무', 'category' => '부채'],
|
||||
['code' => '254', 'name' => '퇴직급여충당부채', 'category' => '부채'],
|
||||
['code' => '255', 'name' => '장기미지급금', 'category' => '부채'],
|
||||
['code' => '256', 'name' => '임대보증금', 'category' => '부채'],
|
||||
['code' => '257', 'name' => '이연법인세부채', 'category' => '부채'],
|
||||
|
||||
// ===== 자본 (3xx) =====
|
||||
['code' => '301', 'name' => '자본금', 'category' => '자본'],
|
||||
['code' => '311', 'name' => '주식발행초과금', 'category' => '자본'],
|
||||
['code' => '312', 'name' => '감자차익', 'category' => '자본'],
|
||||
['code' => '313', 'name' => '자기주식처분이익', 'category' => '자본'],
|
||||
['code' => '321', 'name' => '이익준비금', 'category' => '자본'],
|
||||
['code' => '322', 'name' => '기업합리화적립금', 'category' => '자본'],
|
||||
['code' => '323', 'name' => '재무구조개선적립금', 'category' => '자본'],
|
||||
['code' => '324', 'name' => '임의적립금', 'category' => '자본'],
|
||||
['code' => '331', 'name' => '이월이익잉여금', 'category' => '자본'],
|
||||
['code' => '332', 'name' => '당기순이익', 'category' => '자본'],
|
||||
['code' => '333', 'name' => '전기이월이익잉여금', 'category' => '자본'],
|
||||
|
||||
// ===== 수익 (4xx) =====
|
||||
['code' => '401', 'name' => '상품매출', 'category' => '수익'],
|
||||
['code' => '402', 'name' => '제품매출', 'category' => '수익'],
|
||||
['code' => '403', 'name' => '공사수입', 'category' => '수익'],
|
||||
['code' => '404', 'name' => '용역수입', 'category' => '수익'],
|
||||
['code' => '405', 'name' => '임대수입', 'category' => '수익'],
|
||||
['code' => '406', 'name' => '수출매출', 'category' => '수익'],
|
||||
['code' => '407', 'name' => '기타매출', 'category' => '수익'],
|
||||
['code' => '408', 'name' => '매출에누리', 'category' => '수익'],
|
||||
['code' => '409', 'name' => '매출환입', 'category' => '수익'],
|
||||
['code' => '410', 'name' => '매출할인', 'category' => '수익'],
|
||||
|
||||
// ===== 매출원가 (5xx) =====
|
||||
['code' => '501', 'name' => '상품매출원가', 'category' => '비용'],
|
||||
['code' => '502', 'name' => '기초상품재고액', 'category' => '비용'],
|
||||
['code' => '503', 'name' => '당기상품매입액', 'category' => '비용'],
|
||||
['code' => '504', 'name' => '기말상품재고액', 'category' => '비용'],
|
||||
['code' => '505', 'name' => '제품매출원가', 'category' => '비용'],
|
||||
['code' => '506', 'name' => '기초제품재고액', 'category' => '비용'],
|
||||
['code' => '507', 'name' => '당기제품제조원가', 'category' => '비용'],
|
||||
['code' => '508', 'name' => '기말제품재고액', 'category' => '비용'],
|
||||
['code' => '509', 'name' => '타계정대체', 'category' => '비용'],
|
||||
|
||||
// ===== 판매비와관리비 (8xx) =====
|
||||
['code' => '801', 'name' => '급여', 'category' => '비용'],
|
||||
['code' => '802', 'name' => '상여금', 'category' => '비용'],
|
||||
['code' => '803', 'name' => '잡급', 'category' => '비용'],
|
||||
['code' => '804', 'name' => '퇴직급여', 'category' => '비용'],
|
||||
['code' => '805', 'name' => '복리후생비', 'category' => '비용'],
|
||||
['code' => '806', 'name' => '여비교통비', 'category' => '비용'],
|
||||
['code' => '807', 'name' => '접대비', 'category' => '비용'],
|
||||
['code' => '808', 'name' => '통신비', 'category' => '비용'],
|
||||
['code' => '809', 'name' => '수도광열비', 'category' => '비용'],
|
||||
['code' => '810', 'name' => '전력비', 'category' => '비용'],
|
||||
['code' => '811', 'name' => '세금과공과', 'category' => '비용'],
|
||||
['code' => '812', 'name' => '임차료', 'category' => '비용'],
|
||||
['code' => '813', 'name' => '감가상각비', 'category' => '비용'],
|
||||
['code' => '814', 'name' => '무형자산상각비', 'category' => '비용'],
|
||||
['code' => '815', 'name' => '수선비', 'category' => '비용'],
|
||||
['code' => '816', 'name' => '보험료', 'category' => '비용'],
|
||||
['code' => '817', 'name' => '차량유지비', 'category' => '비용'],
|
||||
['code' => '818', 'name' => '운반비', 'category' => '비용'],
|
||||
['code' => '819', 'name' => '교육훈련비', 'category' => '비용'],
|
||||
['code' => '820', 'name' => '도서인쇄비', 'category' => '비용'],
|
||||
['code' => '821', 'name' => '사무용품비', 'category' => '비용'],
|
||||
['code' => '822', 'name' => '소모품비', 'category' => '비용'],
|
||||
['code' => '823', 'name' => '지급수수료', 'category' => '비용'],
|
||||
['code' => '824', 'name' => '광고선전비', 'category' => '비용'],
|
||||
['code' => '825', 'name' => '대손상각비', 'category' => '비용'],
|
||||
['code' => '826', 'name' => '건물관리비', 'category' => '비용'],
|
||||
['code' => '827', 'name' => '경상연구개발비', 'category' => '비용'],
|
||||
['code' => '828', 'name' => '판매수수료', 'category' => '비용'],
|
||||
['code' => '829', 'name' => '판매촉진비', 'category' => '비용'],
|
||||
['code' => '830', 'name' => '포장비', 'category' => '비용'],
|
||||
['code' => '831', 'name' => '하역비', 'category' => '비용'],
|
||||
['code' => '832', 'name' => '보관료', 'category' => '비용'],
|
||||
['code' => '833', 'name' => '견본비', 'category' => '비용'],
|
||||
['code' => '834', 'name' => '회의비', 'category' => '비용'],
|
||||
['code' => '835', 'name' => '잡비', 'category' => '비용'],
|
||||
['code' => '836', 'name' => '외주가공비', 'category' => '비용'],
|
||||
['code' => '837', 'name' => '리스료', 'category' => '비용'],
|
||||
['code' => '838', 'name' => '용역비', 'category' => '비용'],
|
||||
|
||||
// ===== 영업외수익 (9xx) =====
|
||||
['code' => '901', 'name' => '이자수익', 'category' => '수익'],
|
||||
['code' => '902', 'name' => '배당금수익', 'category' => '수익'],
|
||||
['code' => '903', 'name' => '임대료수익', 'category' => '수익'],
|
||||
['code' => '904', 'name' => '유가증권처분이익', 'category' => '수익'],
|
||||
['code' => '905', 'name' => '유형자산처분이익', 'category' => '수익'],
|
||||
['code' => '906', 'name' => '외환차익', 'category' => '수익'],
|
||||
['code' => '907', 'name' => '외화환산이익', 'category' => '수익'],
|
||||
['code' => '908', 'name' => '대손충당금환입', 'category' => '수익'],
|
||||
['code' => '909', 'name' => '잡이익', 'category' => '수익'],
|
||||
|
||||
// ===== 영업외비용 (9xx) =====
|
||||
['code' => '951', 'name' => '이자비용', 'category' => '비용'],
|
||||
['code' => '952', 'name' => '기부금', 'category' => '비용'],
|
||||
['code' => '953', 'name' => '유가증권처분손실', 'category' => '비용'],
|
||||
['code' => '954', 'name' => '유형자산처분손실', 'category' => '비용'],
|
||||
['code' => '955', 'name' => '재고자산감모손실', 'category' => '비용'],
|
||||
['code' => '956', 'name' => '외환차손', 'category' => '비용'],
|
||||
['code' => '957', 'name' => '외화환산손실', 'category' => '비용'],
|
||||
['code' => '958', 'name' => '잡손실', 'category' => '비용'],
|
||||
|
||||
// ===== 법인세비용 =====
|
||||
['code' => '991', 'name' => '법인세비용', 'category' => '비용'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,10 @@
|
||||
accounts: '{{ route("barobill.eaccount.accounts") }}',
|
||||
transactions: '{{ route("barobill.eaccount.transactions") }}',
|
||||
accountCodes: '{{ route("barobill.eaccount.account-codes") }}',
|
||||
accountCodesAll: '{{ route("barobill.eaccount.account-codes.all") }}',
|
||||
accountCodesStore: '{{ route("barobill.eaccount.account-codes.store") }}',
|
||||
accountCodesUpdate: (id) => `/barobill/eaccount/account-codes/${id}`,
|
||||
accountCodesDestroy: (id) => `/barobill/eaccount/account-codes/${id}`,
|
||||
save: '{{ route("barobill.eaccount.save") }}',
|
||||
export: '{{ route("barobill.eaccount.export") }}',
|
||||
};
|
||||
@@ -167,6 +171,279 @@ className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-2
|
||||
</select>
|
||||
);
|
||||
|
||||
// AccountCodeSettingsModal Component
|
||||
const AccountCodeSettingsModal = ({ isOpen, onClose, onUpdate }) => {
|
||||
const [codes, setCodes] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newCode, setNewCode] = useState('');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newCategory, setNewCategory] = useState('');
|
||||
const [filter, setFilter] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
|
||||
const categories = ['자산', '부채', '자본', '수익', '비용'];
|
||||
|
||||
// 모달 열릴 때 데이터 로드
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadCodes();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const loadCodes = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(API.accountCodesAll);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setCodes(data.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
notify('계정과목 로드 실패', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!newCode.trim() || !newName.trim()) {
|
||||
notify('코드와 이름을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(API.accountCodesStore, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: newCode.trim(),
|
||||
name: newName.trim(),
|
||||
category: newCategory || null
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
notify('계정과목이 추가되었습니다.', 'success');
|
||||
setNewCode('');
|
||||
setNewName('');
|
||||
setNewCategory('');
|
||||
loadCodes();
|
||||
onUpdate();
|
||||
} else {
|
||||
notify(data.error || '추가 실패', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
notify('추가 실패: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = async (item) => {
|
||||
try {
|
||||
const res = await fetch(API.accountCodesUpdate(item.id), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({ is_active: !item.is_active })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
loadCodes();
|
||||
onUpdate();
|
||||
}
|
||||
} catch (err) {
|
||||
notify('변경 실패', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
if (!confirm(`"${item.code} ${item.name}" 계정과목을 삭제하시겠습니까?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(API.accountCodesDestroy(item.id), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
notify('삭제되었습니다.', 'success');
|
||||
loadCodes();
|
||||
onUpdate();
|
||||
} else {
|
||||
notify(data.error || '삭제 실패', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
notify('삭제 실패: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCodes = codes.filter(c => {
|
||||
const matchText = filter === '' ||
|
||||
c.code.includes(filter) ||
|
||||
c.name.includes(filter);
|
||||
const matchCategory = categoryFilter === '' || c.category === categoryFilter;
|
||||
return matchText && matchCategory;
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stone-200 bg-stone-50">
|
||||
<h2 className="text-lg font-bold text-stone-900">계정과목 설정</h2>
|
||||
<button onClick={onClose} className="text-stone-400 hover:text-stone-600 text-2xl">×</button>
|
||||
</div>
|
||||
|
||||
{/* Add Form */}
|
||||
<div className="px-6 py-4 border-b border-stone-100 bg-emerald-50/50">
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">코드</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newCode}
|
||||
onChange={(e) => setNewCode(e.target.value)}
|
||||
placeholder="예: 101"
|
||||
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[2]">
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">계정과목명</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="예: 현금"
|
||||
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">분류</label>
|
||||
<select
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 text-sm font-medium"
|
||||
>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="px-6 py-3 border-b border-stone-100 flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="코드 또는 이름 검색..."
|
||||
className="flex-1 px-3 py-2 border border-stone-200 rounded-lg text-sm"
|
||||
/>
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-stone-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">전체 분류</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-stone-500 py-2">
|
||||
{filteredCodes.length}개
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="overflow-y-auto" style={{maxHeight: '400px'}}>
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-stone-400">로딩 중...</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-stone-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-medium text-stone-600">코드</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-stone-600">계정과목명</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-stone-600">분류</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-stone-600">상태</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-stone-600">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-stone-100">
|
||||
{filteredCodes.map(item => (
|
||||
<tr key={item.id} className={`hover:bg-stone-50 ${!item.is_active ? 'opacity-50' : ''}`}>
|
||||
<td className="px-4 py-2 font-mono">{item.code}</td>
|
||||
<td className="px-4 py-2">{item.name}</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
item.category === '자산' ? 'bg-blue-100 text-blue-700' :
|
||||
item.category === '부채' ? 'bg-red-100 text-red-700' :
|
||||
item.category === '자본' ? 'bg-purple-100 text-purple-700' :
|
||||
item.category === '수익' ? 'bg-green-100 text-green-700' :
|
||||
item.category === '비용' ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-stone-100 text-stone-600'
|
||||
}`}>
|
||||
{item.category || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<button
|
||||
onClick={() => handleToggleActive(item)}
|
||||
className={`px-2 py-1 rounded text-xs ${
|
||||
item.is_active
|
||||
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||
: 'bg-stone-100 text-stone-500 hover:bg-stone-200'
|
||||
}`}
|
||||
>
|
||||
{item.is_active ? '사용중' : '미사용'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(item)}
|
||||
className="text-red-500 hover:text-red-700 text-xs"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-stone-200 bg-stone-50 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 bg-stone-600 text-white rounded-lg hover:bg-stone-700 font-medium"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TransactionTable Component
|
||||
const TransactionTable = ({
|
||||
logs,
|
||||
@@ -183,6 +460,7 @@ className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-2
|
||||
onCastChange,
|
||||
onSave,
|
||||
onExport,
|
||||
onOpenSettings,
|
||||
saving,
|
||||
hasChanges
|
||||
}) => {
|
||||
@@ -274,6 +552,16 @@ className="flex items-center gap-2 px-4 py-2 bg-blue-100 text-blue-700 rounded-l
|
||||
</svg>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded-lg text-sm font-medium hover:bg-stone-200 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
계정과목 설정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto" style={ {maxHeight: '500px', overflowY: 'auto'} }>
|
||||
@@ -367,6 +655,7 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2
|
||||
const [error, setError] = useState(null);
|
||||
const [accountCodes, setAccountCodes] = useState([]);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
|
||||
// 날짜 필터 상태 (기본: 현재 월)
|
||||
const currentMonth = getMonthDates(0);
|
||||
@@ -633,11 +922,19 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2
|
||||
onCastChange={handleCastChange}
|
||||
onSave={handleSave}
|
||||
onExport={handleExport}
|
||||
onOpenSettings={() => setShowSettingsModal(true)}
|
||||
saving={saving}
|
||||
hasChanges={hasChanges}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Account Code Settings Modal */}
|
||||
<AccountCodeSettingsModal
|
||||
isOpen={showSettingsModal}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
onUpdate={loadAccountCodes}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
{!error && pagination.maxPageNum > 1 && (
|
||||
<div className="flex justify-center gap-2">
|
||||
|
||||
@@ -293,6 +293,10 @@
|
||||
Route::get('/accounts', [\App\Http\Controllers\Barobill\EaccountController::class, 'accounts'])->name('accounts');
|
||||
Route::get('/transactions', [\App\Http\Controllers\Barobill\EaccountController::class, 'transactions'])->name('transactions');
|
||||
Route::get('/account-codes', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodes'])->name('account-codes');
|
||||
Route::get('/account-codes/all', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodesAll'])->name('account-codes.all');
|
||||
Route::post('/account-codes', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodeStore'])->name('account-codes.store');
|
||||
Route::put('/account-codes/{id}', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodeUpdate'])->name('account-codes.update');
|
||||
Route::delete('/account-codes/{id}', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodeDestroy'])->name('account-codes.destroy');
|
||||
Route::post('/save', [\App\Http\Controllers\Barobill\EaccountController::class, 'save'])->name('save');
|
||||
Route::get('/export', [\App\Http\Controllers\Barobill\EaccountController::class, 'exportExcel'])->name('export');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user