chore: [env] .env.example 업데이트 및 .gitignore 정리
- .env.example을 SAM 프로젝트 실제 키 구조로 업데이트 - .gitignore에 !.env.example 예외 추가 - GCS_* 중복 키 제거, Gemini/Claude/Vertex 키 섹션 추가
This commit is contained in:
137
.env.example
Normal file
137
.env.example
Normal file
@@ -0,0 +1,137 @@
|
||||
# ─────────────────────────────────────────────────
|
||||
# SAM API (REST API 서버) 환경 변수
|
||||
# ─────────────────────────────────────────────────
|
||||
# 이 파일을 .env로 복사한 후 실제 값을 입력하세요.
|
||||
# cp .env.example .env && php artisan key:generate
|
||||
# ─────────────────────────────────────────────────
|
||||
|
||||
APP_NAME="SAM API"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://api.sam.kr/
|
||||
|
||||
APP_LOCALE=ko
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=ko_KR
|
||||
APP_TIMEZONE=Asia/Seoul
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# ─── Slack 로그 알림 ───
|
||||
LOG_SLACK_WEBHOOK_URL=
|
||||
LOG_SLACK_USERNAME=API_SERVER
|
||||
LOG_SLACK_EMOJI=:boom:
|
||||
|
||||
# ─── Database ───
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=samdb
|
||||
DB_USERNAME=samuser
|
||||
DB_PASSWORD=sampass
|
||||
# 도커 환경: docker-compose.yml의 환경변수로 오버라이드 (DB_HOST=sam-mysql-1)
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ─── Mail ───
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# ─── AWS (미사용) ───
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# ─── Swagger ───
|
||||
L5_SWAGGER_GENERATE_ALWAYS=true
|
||||
L5_SWAGGER_CONST_HOST=https://api.sam.kr/
|
||||
L5_SWAGGER_CONST_NAME="SAM API 서버"
|
||||
|
||||
# ─── Sanctum 토큰 만료 (분) ───
|
||||
SANCTUM_ACCESS_TOKEN_EXPIRATION=120
|
||||
SANCTUM_REFRESH_TOKEN_EXPIRATION=10080
|
||||
|
||||
# ─── 내부 통신 키 (MNG ↔ API HMAC 검증) ───
|
||||
# MNG 프로젝트의 INTERNAL_EXCHANGE_SECRET과 동일한 값 사용
|
||||
INTERNAL_EXCHANGE_SECRET=
|
||||
|
||||
# ─── Firebase (FCM) ───
|
||||
FCM_PROJECT_ID=
|
||||
FCM_SA_PATH=secrets/codebridge-x-firebase-sa.json
|
||||
|
||||
# ─── 5130 Legacy DB ───
|
||||
CHANDJ_DB_HOST=sam-mysql-1
|
||||
CHANDJ_DB_DATABASE=chandj
|
||||
CHANDJ_DB_USERNAME=root
|
||||
CHANDJ_DB_PASSWORD=root
|
||||
|
||||
# ─── 바로빌 SOAP API ───
|
||||
BAROBILL_CERT_KEY_TEST=
|
||||
BAROBILL_CERT_KEY_PROD=
|
||||
BAROBILL_CORP_NUM=
|
||||
BAROBILL_TEST_MODE=true
|
||||
|
||||
# ─────────────────────────────────────────────────
|
||||
# 공유 API 키 (MNG 프로젝트와 동일한 값 사용)
|
||||
# ─────────────────────────────────────────────────
|
||||
|
||||
# ─── Google Gemini AI ───
|
||||
GEMINI_API_KEY=
|
||||
GEMINI_MODEL=gemini-2.0-flash
|
||||
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
|
||||
GEMINI_PROJECT_ID=codebridge-chatbot
|
||||
|
||||
# ─── Claude AI ───
|
||||
CLAUDE_API_KEY=
|
||||
|
||||
# ─── Vertex AI (Veo 영상 생성) ───
|
||||
VERTEX_AI_PROJECT_ID=codebridge-chatbot
|
||||
VERTEX_AI_LOCATION=us-central1
|
||||
|
||||
# ─── Google Cloud (STT + GCS Storage) ───
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/var/www/mng/apikey/google_service_account.json
|
||||
GOOGLE_STORAGE_BUCKET=codebridge-speech-audio-files
|
||||
GOOGLE_STT_LOCATION=asia-southeast1
|
||||
|
||||
# ─── FCM (Firebase Cloud Messaging) ───
|
||||
FCM_BATCH_CHUNK_SIZE=200
|
||||
FCM_BATCH_DELAY_MS=100
|
||||
FCM_LOGGING_ENABLED=true
|
||||
FCM_LOG_CHANNEL=stack
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@
|
||||
!storage/.gitignore
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.phpunit.result.cache
|
||||
Homestead.yaml
|
||||
Homestead.json
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Barobill\BankServiceUrlRequest;
|
||||
use App\Http\Requests\Barobill\BarobillLoginRequest;
|
||||
use App\Http\Requests\Barobill\BarobillSignupRequest;
|
||||
use App\Services\Barobill\BarobillSoapService;
|
||||
|
||||
class BarobillController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private BarobillSoapService $barobillSoapService
|
||||
) {}
|
||||
|
||||
public function login(BarobillLoginRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->registerLogin($request->validated()),
|
||||
__('message.barobill.login_success')
|
||||
);
|
||||
}
|
||||
|
||||
public function signup(BarobillSignupRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->registerSignup($request->validated()),
|
||||
__('message.barobill.signup_success')
|
||||
);
|
||||
}
|
||||
|
||||
public function bankServiceUrl(BankServiceUrlRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->getBankServiceRedirectUrl($request->validated()),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->getIntegrationStatus(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function accountLinkUrl()
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->getAccountLinkRedirectUrl(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function cardLinkUrl()
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->getCardLinkRedirectUrl(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function certificateUrl()
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->barobillSoapService->getCertificateRedirectUrl(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\CorporateCard\StoreCorporateCardRequest;
|
||||
use App\Http\Requests\V1\CorporateCard\UpdateCorporateCardRequest;
|
||||
use App\Services\CorporateCardService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CorporateCardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CorporateCardService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 법인카드 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'search',
|
||||
'status',
|
||||
'card_type',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$cards = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($cards, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 등록
|
||||
*/
|
||||
public function store(StoreCorporateCardRequest $request)
|
||||
{
|
||||
$card = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($card, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$card = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($card, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 수정
|
||||
*/
|
||||
public function update(int $id, UpdateCorporateCardRequest $request)
|
||||
{
|
||||
$card = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($card, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 상태 토글 (사용/정지)
|
||||
*/
|
||||
public function toggle(int $id)
|
||||
{
|
||||
$card = $this->service->toggleStatus($id);
|
||||
|
||||
return ApiResponse::success($card, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 법인카드 목록 (셀렉트박스용)
|
||||
*/
|
||||
public function active()
|
||||
{
|
||||
$cards = $this->service->getActiveCards();
|
||||
|
||||
return ApiResponse::success($cards, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
@@ -4,30 +4,28 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Authz\RoleIndexRequest;
|
||||
use App\Http\Requests\Authz\RoleStoreRequest;
|
||||
use App\Http\Requests\Authz\RoleUpdateRequest;
|
||||
use App\Services\Authz\RoleService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* 역할 목록 조회
|
||||
*/
|
||||
public function index(RoleIndexRequest $request)
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return RoleService::index($request->validated());
|
||||
return RoleService::index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 생성
|
||||
*/
|
||||
public function store(RoleStoreRequest $request)
|
||||
public function store(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return RoleService::store($request->validated());
|
||||
return RoleService::store($request->all());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
@@ -44,10 +42,10 @@ public function show($id)
|
||||
/**
|
||||
* 역할 수정
|
||||
*/
|
||||
public function update(RoleUpdateRequest $request, $id)
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return RoleService::update((int) $id, $request->validated());
|
||||
return RoleService::update((int) $id, $request->all());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,38 +4,37 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Authz\RolePermissionGrantRequest;
|
||||
use App\Http\Requests\Authz\RolePermissionToggleRequest;
|
||||
use App\Services\Authz\RolePermissionService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RolePermissionController extends Controller
|
||||
{
|
||||
public function index($id)
|
||||
public function index($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return RolePermissionService::list((int) $id);
|
||||
}, __('message.fetched'));
|
||||
}, '역할 퍼미션 목록 조회');
|
||||
}
|
||||
|
||||
public function grant($id, RolePermissionGrantRequest $request)
|
||||
public function grant($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return RolePermissionService::grant((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return RolePermissionService::grant((int) $id, $request->all());
|
||||
}, '역할 퍼미션 부여');
|
||||
}
|
||||
|
||||
public function revoke($id, RolePermissionGrantRequest $request)
|
||||
public function revoke($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return RolePermissionService::revoke((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return RolePermissionService::revoke((int) $id, $request->all());
|
||||
}, '역할 퍼미션 회수');
|
||||
}
|
||||
|
||||
public function sync($id, RolePermissionGrantRequest $request)
|
||||
public function sync($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return RolePermissionService::sync((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return RolePermissionService::sync((int) $id, $request->all());
|
||||
}, '역할 퍼미션 동기화');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +60,10 @@ public function matrix($id)
|
||||
/**
|
||||
* 특정 메뉴의 특정 권한 토글
|
||||
*/
|
||||
public function toggle($id, RolePermissionToggleRequest $request)
|
||||
public function toggle($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return RolePermissionService::toggle((int) $id, $request->validated());
|
||||
return RolePermissionService::toggle((int) $id, $request->all());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\TaxInvoice\CancelTaxInvoiceRequest;
|
||||
use App\Http\Requests\TaxInvoice\CreateTaxInvoiceRequest;
|
||||
use App\Http\Requests\TaxInvoice\SaveSupplierSettingsRequest;
|
||||
use App\Http\Requests\TaxInvoice\TaxInvoiceListRequest;
|
||||
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
||||
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
||||
@@ -24,9 +23,11 @@ public function __construct(
|
||||
*/
|
||||
public function index(TaxInvoiceListRequest $request)
|
||||
{
|
||||
$taxInvoices = $this->taxInvoiceService->list($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->list($request->validated()),
|
||||
__('message.fetched')
|
||||
data: $taxInvoices,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,9 +36,11 @@ public function index(TaxInvoiceListRequest $request)
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->show($id),
|
||||
__('message.fetched')
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,9 +49,12 @@ public function show(int $id)
|
||||
*/
|
||||
public function store(CreateTaxInvoiceRequest $request)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->create($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->create($request->validated()),
|
||||
__('message.created')
|
||||
data: $taxInvoice,
|
||||
message: __('message.created'),
|
||||
status: 201
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,9 +63,11 @@ public function store(CreateTaxInvoiceRequest $request)
|
||||
*/
|
||||
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->update($id, $request->validated()),
|
||||
__('message.updated')
|
||||
data: $taxInvoice,
|
||||
message: __('message.updated')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,9 +76,11 @@ public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->taxInvoiceService->delete($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->delete($id),
|
||||
__('message.deleted')
|
||||
data: null,
|
||||
message: __('message.deleted')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,9 +89,11 @@ public function destroy(int $id)
|
||||
*/
|
||||
public function issue(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->issue($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->issue($id),
|
||||
__('message.tax_invoice.issued')
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.issued')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,9 +102,11 @@ public function issue(int $id)
|
||||
*/
|
||||
public function bulkIssue(BulkIssueRequest $request)
|
||||
{
|
||||
$result = $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->bulkIssue($request->getIds()),
|
||||
__('message.tax_invoice.bulk_issued')
|
||||
data: $result,
|
||||
message: __('message.tax_invoice.bulk_issued')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,9 +115,11 @@ public function bulkIssue(BulkIssueRequest $request)
|
||||
*/
|
||||
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->cancel($id, $request->validated()['reason']),
|
||||
__('message.tax_invoice.cancelled')
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.cancelled')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,9 +128,11 @@ public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function checkStatus(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->checkStatus($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->checkStatus($id),
|
||||
__('message.fetched')
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,42 +141,11 @@ public function checkStatus(int $id)
|
||||
*/
|
||||
public function summary(TaxInvoiceSummaryRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->summary($request->validated()),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
$summary = $this->taxInvoiceService->summary($request->validated());
|
||||
|
||||
/**
|
||||
* 공급자 설정 조회
|
||||
*/
|
||||
public function supplierSettings()
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->getSupplierSettings(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공급자 설정 저장
|
||||
*/
|
||||
public function saveSupplierSettings(SaveSupplierSettingsRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->saveSupplierSettings($request->validated()),
|
||||
__('message.updated')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 생성 + 즉시 발행
|
||||
*/
|
||||
public function storeAndIssue(CreateTaxInvoiceRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->taxInvoiceService->createAndIssue($request->validated()),
|
||||
__('message.tax_invoice.issued')
|
||||
data: $summary,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Authz\UserRoleGrantRequest;
|
||||
use App\Services\Authz\UserRoleService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserRoleController extends Controller
|
||||
{
|
||||
@@ -13,27 +13,27 @@ public function index($id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return UserRoleService::list((int) $id);
|
||||
}, __('message.fetched'));
|
||||
}, '사용자의 역할 목록 조회');
|
||||
}
|
||||
|
||||
public function grant($id, UserRoleGrantRequest $request)
|
||||
public function grant($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return UserRoleService::grant((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return UserRoleService::grant((int) $id, $request->all());
|
||||
}, '사용자에게 역할 부여');
|
||||
}
|
||||
|
||||
public function revoke($id, UserRoleGrantRequest $request)
|
||||
public function revoke($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return UserRoleService::revoke((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return UserRoleService::revoke((int) $id, $request->all());
|
||||
}, '사용자의 역할 회수');
|
||||
}
|
||||
|
||||
public function sync($id, UserRoleGrantRequest $request)
|
||||
public function sync($id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return UserRoleService::sync((int) $id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
return UserRoleService::sync((int) $id, $request->all());
|
||||
}, '사용자의 역할 동기화');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RoleIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'page' => 'sometimes|integer|min:1',
|
||||
'size' => 'sometimes|integer|min:1|max:100',
|
||||
'q' => 'sometimes|nullable|string|max:100',
|
||||
'is_hidden' => 'sometimes|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RolePermissionGrantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'permission_names' => 'sometimes|array',
|
||||
'permission_names.*' => 'string|min:1',
|
||||
'menus' => 'sometimes|array',
|
||||
'menus.*' => 'integer|min:1',
|
||||
'actions' => 'sometimes|array',
|
||||
'actions.*' => [
|
||||
'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$data = $this->all();
|
||||
if (empty($data['permission_names']) && (empty($data['menus']) || empty($data['actions']))) {
|
||||
$validator->errors()->add('permission_names', __('error.role.permission_input_required'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RolePermissionToggleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$permissionTypes = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']);
|
||||
|
||||
return [
|
||||
'menu_id' => 'required|integer|min:1',
|
||||
'permission_type' => ['required', 'string', Rule::in($permissionTypes)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RoleStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tenantId = (int) app('tenant_id');
|
||||
$guard = 'api';
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
'required', 'string', 'max:100',
|
||||
Rule::unique('roles', 'name')->where(fn ($q) => $q
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('guard_name', $guard)),
|
||||
],
|
||||
'description' => 'nullable|string|max:255',
|
||||
'is_hidden' => 'sometimes|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RoleUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tenantId = (int) app('tenant_id');
|
||||
$guard = 'api';
|
||||
$roleId = (int) $this->route('id');
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
'sometimes', 'string', 'max:100',
|
||||
Rule::unique('roles', 'name')
|
||||
->where(fn ($q) => $q->where('tenant_id', $tenantId)->where('guard_name', $guard))
|
||||
->ignore($roleId),
|
||||
],
|
||||
'description' => 'sometimes|nullable|string|max:255',
|
||||
'is_hidden' => 'sometimes|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Authz;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UserRoleGrantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role_names' => 'sometimes|array',
|
||||
'role_names.*' => 'string|min:1',
|
||||
'role_ids' => 'sometimes|array',
|
||||
'role_ids.*' => 'integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$data = $this->all();
|
||||
if (empty($data['role_names']) && empty($data['role_ids'])) {
|
||||
$validator->errors()->add('role_names', __('error.role.role_input_required'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Barobill;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BankServiceUrlRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'bank_code' => ['required', 'string', 'max:10'],
|
||||
'account_type' => ['required', 'string', 'max:10'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Barobill;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BarobillLoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'barobill_id' => ['required', 'string', 'max:50'],
|
||||
'password' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Barobill;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BarobillSignupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'business_number' => ['required', 'string', 'max:20'],
|
||||
'company_name' => ['required', 'string', 'max:100'],
|
||||
'ceo_name' => ['required', 'string', 'max:50'],
|
||||
'business_type' => ['nullable', 'string', 'max:50'],
|
||||
'business_category' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:255'],
|
||||
'barobill_id' => ['required', 'string', 'max:50'],
|
||||
'password' => ['required', 'string', 'max:255'],
|
||||
'manager_name' => ['nullable', 'string', 'max:50'],
|
||||
'manager_phone' => ['nullable', 'string', 'max:20'],
|
||||
'manager_email' => ['nullable', 'email', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TaxInvoice;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveSupplierSettingsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'business_number' => ['required', 'string', 'max:20'],
|
||||
'company_name' => ['required', 'string', 'max:100'],
|
||||
'representative_name' => ['required', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:100'],
|
||||
'business_item' => ['nullable', 'string', 'max:100'],
|
||||
'contact_name' => ['nullable', 'string', 'max:50'],
|
||||
'contact_phone' => ['nullable', 'string', 'max:20'],
|
||||
'contact_email' => ['nullable', 'email', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,9 @@ public function rules(): array
|
||||
'account_number' => ['required', 'string', 'max:30', 'regex:/^[\d-]+$/'],
|
||||
'account_holder' => ['required', 'string', 'max:50'],
|
||||
'account_name' => ['required', 'string', 'max:100'],
|
||||
'account_type' => ['nullable', 'string', 'max:30'],
|
||||
'balance' => ['nullable', 'numeric', 'min:0'],
|
||||
'currency' => ['nullable', 'string', 'max:3'],
|
||||
'opened_at' => ['nullable', 'date'],
|
||||
'branch_name' => ['nullable', 'string', 'max:100'],
|
||||
'memo' => ['nullable', 'string', 'max:500'],
|
||||
'status' => ['nullable', 'string', 'in:active,inactive'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'is_primary' => ['nullable', 'boolean'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,8 @@ public function rules(): array
|
||||
'account_number' => ['sometimes', 'string', 'max:30', 'regex:/^[\d-]+$/'],
|
||||
'account_holder' => ['sometimes', 'string', 'max:50'],
|
||||
'account_name' => ['sometimes', 'string', 'max:100'],
|
||||
'account_type' => ['sometimes', 'nullable', 'string', 'max:30'],
|
||||
'balance' => ['sometimes', 'nullable', 'numeric', 'min:0'],
|
||||
'currency' => ['sometimes', 'nullable', 'string', 'max:3'],
|
||||
'opened_at' => ['sometimes', 'nullable', 'date'],
|
||||
'branch_name' => ['sometimes', 'nullable', 'string', 'max:100'],
|
||||
'memo' => ['sometimes', 'nullable', 'string', 'max:500'],
|
||||
'status' => ['sometimes', 'string', 'in:active,inactive'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'sort_order' => ['sometimes', 'nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,12 @@ public function rules(): array
|
||||
{
|
||||
return [
|
||||
'card_company' => ['required', 'string', 'max:50'],
|
||||
'card_type' => ['nullable', 'string', 'max:50'],
|
||||
'card_number' => ['required', 'string', 'regex:/^\d{13,19}$/'],
|
||||
'expiry_date' => ['required', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'],
|
||||
'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'],
|
||||
'card_name' => ['required', 'string', 'max:100'],
|
||||
'alias' => ['nullable', 'string', 'max:100'],
|
||||
'csv' => ['nullable', 'string', 'max:4'],
|
||||
'payment_day' => ['nullable', 'integer', 'min:1', 'max:31'],
|
||||
'total_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
'used_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'remaining_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
'status' => ['nullable', 'string', 'in:active,inactive'],
|
||||
'is_manual' => ['nullable', 'boolean'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'memo' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,12 @@ public function rules(): array
|
||||
{
|
||||
return [
|
||||
'card_company' => ['sometimes', 'string', 'max:50'],
|
||||
'card_type' => ['nullable', 'string', 'max:50'],
|
||||
'card_number' => ['sometimes', 'string', 'regex:/^\d{13,19}$/'],
|
||||
'expiry_date' => ['sometimes', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'],
|
||||
'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'],
|
||||
'card_name' => ['sometimes', 'string', 'max:100'],
|
||||
'alias' => ['nullable', 'string', 'max:100'],
|
||||
'csv' => ['nullable', 'string', 'max:4'],
|
||||
'payment_day' => ['nullable', 'integer', 'min:1', 'max:31'],
|
||||
'total_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
'used_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'remaining_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
'status' => ['sometimes', 'string', 'in:active,inactive'],
|
||||
'is_manual' => ['nullable', 'boolean'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'memo' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\CorporateCard;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreCorporateCardRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'card_name' => ['required', 'string', 'max:100'],
|
||||
'card_company' => ['required', 'string', 'max:50'],
|
||||
'card_number' => ['required', 'string', 'max:30'],
|
||||
'card_type' => ['required', 'string', 'in:credit,debit'],
|
||||
'payment_day' => ['nullable', 'integer', 'min:1', 'max:31'],
|
||||
'credit_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
'card_holder_name' => ['required', 'string', 'max:100'],
|
||||
'actual_user' => ['required', 'string', 'max:100'],
|
||||
'expiry_date' => ['nullable', 'string', 'max:10'],
|
||||
'cvc' => ['nullable', 'string', 'max:4'],
|
||||
'status' => ['nullable', 'string', 'in:active,inactive'],
|
||||
'memo' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\CorporateCard;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCorporateCardRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'card_name' => ['sometimes', 'string', 'max:100'],
|
||||
'card_company' => ['sometimes', 'string', 'max:50'],
|
||||
'card_number' => ['sometimes', 'string', 'max:30'],
|
||||
'card_type' => ['sometimes', 'string', 'in:credit,debit'],
|
||||
'payment_day' => ['sometimes', 'nullable', 'integer', 'min:1', 'max:31'],
|
||||
'credit_limit' => ['sometimes', 'nullable', 'numeric', 'min:0'],
|
||||
'card_holder_name' => ['sometimes', 'string', 'max:100'],
|
||||
'actual_user' => ['sometimes', 'string', 'max:100'],
|
||||
'expiry_date' => ['sometimes', 'nullable', 'string', 'max:10'],
|
||||
'cvc' => ['sometimes', 'nullable', 'string', 'max:4'],
|
||||
'status' => ['sometimes', 'string', 'in:active,inactive'],
|
||||
'memo' => ['sometimes', 'nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,13 @@
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperRole
|
||||
*/
|
||||
class Role extends SpatieRole
|
||||
class Role extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
@@ -35,6 +34,14 @@ class Role extends SpatieRole
|
||||
'tenant_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 관계: 메뉴 권한 (다대다)
|
||||
*/
|
||||
public function menuPermissions()
|
||||
{
|
||||
return $this->hasMany(RoleMenuPermission::class, 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 테넌트
|
||||
*/
|
||||
@@ -54,7 +61,7 @@ public function userRoles()
|
||||
/**
|
||||
* 관계: 사용자 (user_roles 테이블 통해)
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
User::class,
|
||||
@@ -64,6 +71,19 @@ public function users(): BelongsToMany
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 권한 (role_has_permissions 테이블 통해)
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Permission::class,
|
||||
'role_has_permissions',
|
||||
'role_id',
|
||||
'permission_id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프: 공개된 역할만
|
||||
*/
|
||||
|
||||
@@ -40,34 +40,16 @@ class BankAccount extends Model
|
||||
'account_number',
|
||||
'account_holder',
|
||||
'account_name',
|
||||
'account_type',
|
||||
'balance',
|
||||
'currency',
|
||||
'opened_at',
|
||||
'last_transaction_at',
|
||||
'branch_name',
|
||||
'memo',
|
||||
'status',
|
||||
'assigned_user_id',
|
||||
'is_primary',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'balance' => 'decimal:2',
|
||||
'is_primary' => 'boolean',
|
||||
'opened_at' => 'date',
|
||||
'last_transaction_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
@@ -107,69 +89,22 @@ public function updater(): BelongsTo
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
// =========================================================================
|
||||
// Accessors
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 포맷된 잔액
|
||||
*/
|
||||
public function getFormattedBalanceAttribute(): string
|
||||
{
|
||||
$amount = abs($this->balance ?? 0);
|
||||
|
||||
if ($amount >= 100000000) {
|
||||
return number_format($amount / 100000000, 1).'억원';
|
||||
} elseif ($amount >= 10000000) {
|
||||
return number_format($amount / 10000000, 0).'천만원';
|
||||
} elseif ($amount >= 10000) {
|
||||
return number_format($amount / 10000, 0).'만원';
|
||||
}
|
||||
|
||||
return number_format($amount).'원';
|
||||
}
|
||||
|
||||
/**
|
||||
* 마스킹된 계좌번호
|
||||
* 마스킹된 계좌번호 조회
|
||||
*/
|
||||
public function getMaskedAccountNumber(): string
|
||||
{
|
||||
$number = $this->account_number;
|
||||
if (strlen($number) <= 6) {
|
||||
return $number;
|
||||
$length = strlen($this->account_number);
|
||||
if ($length <= 4) {
|
||||
return $this->account_number;
|
||||
}
|
||||
|
||||
return substr($number, 0, 3).'-***-'.substr($number, -4);
|
||||
$visibleEnd = substr($this->account_number, -4);
|
||||
$maskedPart = str_repeat('*', $length - 4);
|
||||
|
||||
return $maskedPart.$visibleEnd;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function scopePrimary($query)
|
||||
{
|
||||
return $query->where('is_primary', true);
|
||||
}
|
||||
|
||||
public function scopeByType($query, string $accountType)
|
||||
{
|
||||
return $query->where('account_type', $accountType);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('bank_name');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 활성 상태 여부
|
||||
*/
|
||||
@@ -187,13 +122,10 @@ public function toggleStatus(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* 잔액 업데이트
|
||||
* 대표계좌로 설정
|
||||
*/
|
||||
public function updateBalance(float $newBalance): void
|
||||
public function setAsPrimary(): void
|
||||
{
|
||||
$this->update([
|
||||
'balance' => $newBalance,
|
||||
'last_transaction_at' => now(),
|
||||
]);
|
||||
$this->is_primary = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BarobillConfig extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'environment',
|
||||
'cert_key',
|
||||
'corp_num',
|
||||
'base_url',
|
||||
'description',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public static function getActiveTest(): ?self
|
||||
{
|
||||
return self::where('environment', 'test')->first();
|
||||
}
|
||||
|
||||
public static function getActiveProduction(): ?self
|
||||
{
|
||||
return self::where('environment', 'production')->first();
|
||||
}
|
||||
|
||||
public static function getActive(bool $isTestMode = false): ?self
|
||||
{
|
||||
return $isTestMode ? self::getActiveTest() : self::getActiveProduction();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BarobillMember extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'barobill_members';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'biz_no',
|
||||
'corp_name',
|
||||
'ceo_name',
|
||||
'addr',
|
||||
'biz_type',
|
||||
'biz_class',
|
||||
'barobill_id',
|
||||
'barobill_pwd',
|
||||
'manager_name',
|
||||
'manager_email',
|
||||
'manager_hp',
|
||||
'status',
|
||||
'server_mode',
|
||||
'last_sales_fetch_at',
|
||||
'last_purchases_fetch_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'barobill_pwd' => 'encrypted',
|
||||
'last_sales_fetch_at' => 'datetime',
|
||||
'last_purchases_fetch_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'barobill_pwd',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function getFormattedBizNoAttribute(): string
|
||||
{
|
||||
$bizNo = preg_replace('/[^0-9]/', '', $this->biz_no);
|
||||
if (strlen($bizNo) === 10) {
|
||||
return substr($bizNo, 0, 3).'-'.substr($bizNo, 3, 2).'-'.substr($bizNo, 5);
|
||||
}
|
||||
|
||||
return $this->biz_no;
|
||||
}
|
||||
|
||||
public function isTestMode(): bool
|
||||
{
|
||||
return $this->server_mode !== 'production';
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,8 @@
|
||||
* @property string $expiry_date
|
||||
* @property string|null $card_password_encrypted
|
||||
* @property string $card_name
|
||||
* @property string|null $card_type
|
||||
* @property string|null $alias
|
||||
* @property string|null $cvc_encrypted
|
||||
* @property int|null $payment_day
|
||||
* @property float|null $total_limit
|
||||
* @property float|null $used_amount
|
||||
* @property float|null $remaining_limit
|
||||
* @property string $status
|
||||
* @property bool $is_manual
|
||||
* @property int|null $assigned_user_id
|
||||
* @property string|null $memo
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
@@ -46,22 +37,13 @@ class Card extends Model
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'card_company',
|
||||
'card_type',
|
||||
'card_number_encrypted',
|
||||
'card_number_last4',
|
||||
'expiry_date',
|
||||
'card_password_encrypted',
|
||||
'card_name',
|
||||
'alias',
|
||||
'cvc_encrypted',
|
||||
'payment_day',
|
||||
'total_limit',
|
||||
'used_amount',
|
||||
'remaining_limit',
|
||||
'status',
|
||||
'is_manual',
|
||||
'assigned_user_id',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
@@ -70,29 +52,12 @@ class Card extends Model
|
||||
protected $hidden = [
|
||||
'card_number_encrypted',
|
||||
'card_password_encrypted',
|
||||
'cvc_encrypted',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'csv',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => 'active',
|
||||
'is_manual' => false,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'payment_day' => 'integer',
|
||||
'total_limit' => 'decimal:2',
|
||||
'used_amount' => 'decimal:2',
|
||||
'remaining_limit' => 'decimal:2',
|
||||
'is_manual' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
@@ -162,38 +127,6 @@ public function getDecryptedCardPassword(): ?string
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CVC/CVV 암호화 설정
|
||||
*/
|
||||
public function setCvc(?string $cvc): void
|
||||
{
|
||||
$this->cvc_encrypted = $cvc
|
||||
? Crypt::encryptString($cvc)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CVC/CVV 복호화 조회
|
||||
*/
|
||||
public function getDecryptedCvc(): ?string
|
||||
{
|
||||
return $this->cvc_encrypted
|
||||
? Crypt::decryptString($this->cvc_encrypted)
|
||||
: null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Accessor (JSON 직렬화용)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* CSV(CVC) 복호화 값 — $appends로 JSON 응답에 포함
|
||||
*/
|
||||
public function getCsvAttribute(): ?string
|
||||
{
|
||||
return $this->getDecryptedCvc();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 법인카드 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $card_name
|
||||
* @property string $card_company
|
||||
* @property string $card_number
|
||||
* @property string $card_type
|
||||
* @property int $payment_day
|
||||
* @property float $credit_limit
|
||||
* @property float $current_usage
|
||||
* @property string $card_holder_name
|
||||
* @property string $actual_user
|
||||
* @property string|null $expiry_date
|
||||
* @property string|null $cvc
|
||||
* @property string $status
|
||||
* @property string|null $memo
|
||||
*/
|
||||
class CorporateCard extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'corporate_cards';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'card_name',
|
||||
'card_company',
|
||||
'card_number',
|
||||
'card_type',
|
||||
'payment_day',
|
||||
'credit_limit',
|
||||
'current_usage',
|
||||
'card_holder_name',
|
||||
'actual_user',
|
||||
'expiry_date',
|
||||
'cvc',
|
||||
'status',
|
||||
'memo',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'cvc',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payment_day' => 'integer',
|
||||
'credit_limit' => 'decimal:2',
|
||||
'current_usage' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => 'active',
|
||||
'payment_day' => 15,
|
||||
'credit_limit' => 0,
|
||||
'current_usage' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function scopeByType($query, string $cardType)
|
||||
{
|
||||
return $query->where('card_type', $cardType);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'active';
|
||||
}
|
||||
|
||||
public function toggleStatus(): void
|
||||
{
|
||||
$this->status = $this->status === 'active' ? 'inactive' : 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* 마스킹된 카드번호
|
||||
*/
|
||||
public function getMaskedCardNumber(): string
|
||||
{
|
||||
$number = preg_replace('/[^0-9]/', '', $this->card_number);
|
||||
if (strlen($number) <= 4) {
|
||||
return $this->card_number;
|
||||
}
|
||||
|
||||
return '****-****-****-'.substr($number, -4);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Services\Authz;
|
||||
|
||||
use App\Models\Permissions\Role;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
class RolePermissionService
|
||||
@@ -16,13 +18,6 @@ protected static function setTeam(int $tenantId): void
|
||||
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);
|
||||
}
|
||||
|
||||
/** 권한 캐시 무효화 */
|
||||
protected static function invalidateCache(int $tenantId): void
|
||||
{
|
||||
AccessService::bumpVersion($tenantId);
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
/** 역할 로드 (테넌트/가드 검증) */
|
||||
protected static function loadRoleOrError(int $roleId, int $tenantId): ?Role
|
||||
{
|
||||
@@ -42,6 +37,7 @@ protected static function resolvePermissionNames(int $tenantId, array $params):
|
||||
$names = [];
|
||||
|
||||
if (! empty($params['permission_names']) && is_array($params['permission_names'])) {
|
||||
// 문자열 배열만 추림
|
||||
foreach ($params['permission_names'] as $n) {
|
||||
if (is_string($n) && $n !== '') {
|
||||
$names[] = trim($n);
|
||||
@@ -87,7 +83,7 @@ public static function list(int $roleId)
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
@@ -108,20 +104,37 @@ public static function grant(int $roleId, array $params = [])
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
// 유효성: 두 방식 중 하나만 요구하진 않지만, 최소 하나는 있어야 함
|
||||
$v = Validator::make($params, [
|
||||
'permission_names' => 'sometimes|array',
|
||||
'permission_names.*' => 'string|min:1',
|
||||
'menus' => 'sometimes|array',
|
||||
'menus.*' => 'integer|min:1',
|
||||
'actions' => 'sometimes|array',
|
||||
'actions.*' => [
|
||||
'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])),
|
||||
],
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['permission_names']) && (empty($params['menus']) || empty($params['actions']))) {
|
||||
return ['error' => 'permission_names 또는 menus+actions 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolvePermissionNames($tenantId, $params);
|
||||
if (empty($names)) {
|
||||
return ['error' => __('error.role.no_valid_permissions'), 'code' => 422];
|
||||
return ['error' => '유효한 퍼미션이 없습니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
// Spatie: 이름 배열 부여 OK (teams 컨텍스트 적용됨)
|
||||
$role->givePermissionTo($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -132,20 +145,35 @@ public static function revoke(int $roleId, array $params = [])
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'permission_names' => 'sometimes|array',
|
||||
'permission_names.*' => 'string|min:1',
|
||||
'menus' => 'sometimes|array',
|
||||
'menus.*' => 'integer|min:1',
|
||||
'actions' => 'sometimes|array',
|
||||
'actions.*' => [
|
||||
'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])),
|
||||
],
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['permission_names']) && (empty($params['menus']) || empty($params['actions']))) {
|
||||
return ['error' => 'permission_names 또는 menus+actions 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolvePermissionNames($tenantId, $params);
|
||||
if (empty($names)) {
|
||||
return ['error' => __('error.role.no_valid_permissions'), 'code' => 422];
|
||||
return ['error' => '유효한 퍼미션이 없습니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
$role->revokePermissionTo($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -156,16 +184,32 @@ public static function sync(int $roleId, array $params = [])
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'permission_names' => 'sometimes|array',
|
||||
'permission_names.*' => 'string|min:1',
|
||||
'menus' => 'sometimes|array',
|
||||
'menus.*' => 'integer|min:1',
|
||||
'actions' => 'sometimes|array',
|
||||
'actions.*' => [
|
||||
'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])),
|
||||
],
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['permission_names']) && (empty($params['menus']) || empty($params['actions']))) {
|
||||
return ['error' => 'permission_names 또는 menus+actions 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolvePermissionNames($tenantId, $params);
|
||||
$names = self::resolvePermissionNames($tenantId, $params); // 존재하지 않으면 생성
|
||||
// 동기화
|
||||
$role->syncPermissions($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -182,7 +226,7 @@ public static function matrix(int $roleId)
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
@@ -232,6 +276,7 @@ public static function menus()
|
||||
->orderBy('id', 'asc')
|
||||
->get(['id', 'parent_id', 'name', 'url', 'icon', 'sort_order', 'is_active']);
|
||||
|
||||
// 트리 구조를 플랫한 배열로 변환 (depth 정보 포함)
|
||||
$flatMenus = self::flattenMenuTree($menus->toArray(), null, 0);
|
||||
|
||||
return [
|
||||
@@ -253,6 +298,7 @@ protected static function flattenMenuTree(array $menus, ?int $parentId = null, i
|
||||
$menu['has_children'] = count(array_filter($menus, fn ($m) => $m['parent_id'] === $menu['id'])) > 0;
|
||||
$result[] = $menu;
|
||||
|
||||
// 자식 메뉴 재귀적으로 추가
|
||||
$children = self::flattenMenuTree($menus, $menu['id'], $depth + 1);
|
||||
$result = array_merge($result, $children);
|
||||
}
|
||||
@@ -267,7 +313,15 @@ public static function toggle(int $roleId, array $params = [])
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'menu_id' => 'required|integer|min:1',
|
||||
'permission_type' => ['required', 'string', Rule::in(self::getPermissionTypes())],
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
|
||||
$menuId = (int) $params['menu_id'];
|
||||
@@ -291,12 +345,14 @@ public static function toggle(int $roleId, array $params = [])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
// 권한 제거
|
||||
\Illuminate\Support\Facades\DB::table('role_has_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permission->id)
|
||||
->delete();
|
||||
$newValue = false;
|
||||
} else {
|
||||
// 권한 부여
|
||||
\Illuminate\Support\Facades\DB::table('role_has_permissions')->insert([
|
||||
'role_id' => $roleId,
|
||||
'permission_id' => $permission->id,
|
||||
@@ -307,8 +363,6 @@ public static function toggle(int $roleId, array $params = [])
|
||||
// 하위 메뉴에 권한 전파
|
||||
self::propagateToChildren($roleId, $menuId, $permissionType, $newValue, $tenantId);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return [
|
||||
'menu_id' => $menuId,
|
||||
'permission_type' => $permissionType,
|
||||
@@ -332,6 +386,7 @@ protected static function propagateToChildren(int $roleId, int $parentMenuId, st
|
||||
]);
|
||||
|
||||
if ($value) {
|
||||
// 권한 부여
|
||||
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permission->id)
|
||||
@@ -344,12 +399,14 @@ protected static function propagateToChildren(int $roleId, int $parentMenuId, st
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// 권한 제거
|
||||
\Illuminate\Support\Facades\DB::table('role_has_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permission->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// 재귀적으로 하위 메뉴 처리
|
||||
self::propagateToChildren($roleId, $child->id, $permissionType, $value, $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -361,7 +418,7 @@ public static function allowAll(int $roleId)
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
@@ -381,6 +438,7 @@ public static function allowAll(int $roleId)
|
||||
'tenant_id' => $tenantId,
|
||||
]);
|
||||
|
||||
// 권한 부여
|
||||
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permission->id)
|
||||
@@ -395,8 +453,6 @@ public static function allowAll(int $roleId)
|
||||
}
|
||||
}
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -407,7 +463,7 @@ public static function denyAll(int $roleId)
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
@@ -435,8 +491,6 @@ public static function denyAll(int $roleId)
|
||||
}
|
||||
}
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -447,7 +501,7 @@ public static function reset(int $roleId)
|
||||
|
||||
$role = self::loadRoleOrError($roleId, $tenantId);
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
@@ -468,6 +522,7 @@ public static function reset(int $roleId)
|
||||
'tenant_id' => $tenantId,
|
||||
]);
|
||||
|
||||
// 권한 부여
|
||||
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permission->id)
|
||||
@@ -481,8 +536,6 @@ public static function reset(int $roleId)
|
||||
}
|
||||
}
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
use App\Models\Permissions\Role;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
class RoleService
|
||||
@@ -49,13 +51,28 @@ public static function store(array $params = [])
|
||||
$tenantId = (int) app('tenant_id');
|
||||
$userId = app('api_user');
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'name' => [
|
||||
'required', 'string', 'max:100',
|
||||
Rule::unique('roles', 'name')->where(fn ($q) => $q
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('guard_name', self::$guard)),
|
||||
],
|
||||
'description' => 'nullable|string|max:255',
|
||||
'is_hidden' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
|
||||
// Spatie 팀(테넌트) 컨텍스트
|
||||
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);
|
||||
|
||||
$role = Role::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'guard_name' => self::$guard,
|
||||
'name' => $params['name'],
|
||||
'name' => $v->validated()['name'],
|
||||
'description' => $params['description'] ?? null,
|
||||
'is_hidden' => $params['is_hidden'] ?? false,
|
||||
'created_by' => $userId,
|
||||
@@ -75,7 +92,7 @@ public static function show(int $id)
|
||||
->find($id);
|
||||
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
return $role;
|
||||
@@ -92,10 +109,25 @@ public static function update(int $id, array $params = [])
|
||||
->find($id);
|
||||
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$updateData = $params;
|
||||
$v = Validator::make($params, [
|
||||
'name' => [
|
||||
'sometimes', 'string', 'max:100',
|
||||
Rule::unique('roles', 'name')
|
||||
->where(fn ($q) => $q->where('tenant_id', $tenantId)->where('guard_name', self::$guard))
|
||||
->ignore($role->id),
|
||||
],
|
||||
'description' => 'sometimes|nullable|string|max:255',
|
||||
'is_hidden' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
|
||||
$updateData = $v->validated();
|
||||
$updateData['updated_by'] = $userId;
|
||||
|
||||
$role->fill($updateData)->save();
|
||||
@@ -114,7 +146,7 @@ public static function destroy(int $id)
|
||||
->find($id);
|
||||
|
||||
if (! $role) {
|
||||
return ['error' => __('error.role.not_found'), 'code' => 404];
|
||||
return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($role, $userId) {
|
||||
@@ -126,9 +158,6 @@ public static function destroy(int $id)
|
||||
$role->delete();
|
||||
});
|
||||
|
||||
AccessService::bumpVersion($tenantId);
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\Services\Authz;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Permissions\Role;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
class UserRoleService
|
||||
@@ -16,13 +17,6 @@ protected static function setTeam(int $tenantId): void
|
||||
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);
|
||||
}
|
||||
|
||||
/** 권한 캐시 무효화 */
|
||||
protected static function invalidateCache(int $tenantId): void
|
||||
{
|
||||
AccessService::bumpVersion($tenantId);
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
/** 유저 로드 (존재 체크) */
|
||||
protected static function loadUserOrError(int $userId): ?User
|
||||
{
|
||||
@@ -60,13 +54,14 @@ protected static function resolveRoleNames(int $tenantId, array $params): array
|
||||
// 정제
|
||||
$names = array_values(array_unique(array_filter($names)));
|
||||
|
||||
// 존재 확인
|
||||
// 존재 확인(필요시 에러 처리 확장 가능)
|
||||
if (! empty($names)) {
|
||||
Role::query()
|
||||
$count = Role::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('guard_name', self::$guard)
|
||||
->whereIn('name', $names)
|
||||
->count();
|
||||
// if ($count !== count($names)) { ... 필요시 상세 에러 반환 }
|
||||
}
|
||||
|
||||
return $names;
|
||||
@@ -79,11 +74,12 @@ public static function list(int $userId)
|
||||
|
||||
$user = self::loadUserOrError($userId);
|
||||
if (! $user) {
|
||||
return ['error' => __('error.role.user_not_found'), 'code' => 404];
|
||||
return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
// 현재 테넌트의 역할만
|
||||
$builder = $user->roles()
|
||||
->where('roles.tenant_id', $tenantId)
|
||||
->where('roles.guard_name', self::$guard)
|
||||
@@ -100,19 +96,30 @@ public static function grant(int $userId, array $params = [])
|
||||
|
||||
$user = self::loadUserOrError($userId);
|
||||
if (! $user) {
|
||||
return ['error' => __('error.role.user_not_found'), 'code' => 404];
|
||||
return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'role_names' => 'sometimes|array',
|
||||
'role_names.*' => 'string|min:1',
|
||||
'role_ids' => 'sometimes|array',
|
||||
'role_ids.*' => 'integer|min:1',
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['role_names']) && empty($params['role_ids'])) {
|
||||
return ['error' => 'role_names 또는 role_ids 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolveRoleNames($tenantId, $params);
|
||||
if (empty($names)) {
|
||||
return ['error' => __('error.role.no_valid_roles'), 'code' => 422];
|
||||
return ['error' => '유효한 역할이 없습니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
$user->assignRole($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
$user->assignRole($names); // teams 컨텍스트 적용됨
|
||||
|
||||
return 'success';
|
||||
}
|
||||
@@ -124,19 +131,30 @@ public static function revoke(int $userId, array $params = [])
|
||||
|
||||
$user = self::loadUserOrError($userId);
|
||||
if (! $user) {
|
||||
return ['error' => __('error.role.user_not_found'), 'code' => 404];
|
||||
return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'role_names' => 'sometimes|array',
|
||||
'role_names.*' => 'string|min:1',
|
||||
'role_ids' => 'sometimes|array',
|
||||
'role_ids.*' => 'integer|min:1',
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['role_names']) && empty($params['role_ids'])) {
|
||||
return ['error' => 'role_names 또는 role_ids 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolveRoleNames($tenantId, $params);
|
||||
if (empty($names)) {
|
||||
return ['error' => __('error.role.no_valid_roles'), 'code' => 422];
|
||||
return ['error' => '유효한 역할이 없습니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
$user->removeRole($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
$user->removeRole($names); // 배열 허용
|
||||
|
||||
return 'success';
|
||||
}
|
||||
@@ -148,20 +166,32 @@ public static function sync(int $userId, array $params = [])
|
||||
|
||||
$user = self::loadUserOrError($userId);
|
||||
if (! $user) {
|
||||
return ['error' => __('error.role.user_not_found'), 'code' => 404];
|
||||
return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404];
|
||||
}
|
||||
|
||||
$v = Validator::make($params, [
|
||||
'role_names' => 'sometimes|array',
|
||||
'role_names.*' => 'string|min:1',
|
||||
'role_ids' => 'sometimes|array',
|
||||
'role_ids.*' => 'integer|min:1',
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return ['error' => $v->errors()->first(), 'code' => 422];
|
||||
}
|
||||
if (empty($params['role_names']) && empty($params['role_ids'])) {
|
||||
return ['error' => 'role_names 또는 role_ids 중 하나는 필요합니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
self::setTeam($tenantId);
|
||||
|
||||
$names = self::resolveRoleNames($tenantId, $params);
|
||||
if (empty($names)) {
|
||||
return ['error' => __('error.role.no_valid_roles'), 'code' => 422];
|
||||
// 정책상 빈 목록 sync 허용 시: $user->syncRoles([]) 로 전부 제거 가능
|
||||
return ['error' => '유효한 역할이 없습니다.', 'code' => 422];
|
||||
}
|
||||
|
||||
$user->syncRoles($names);
|
||||
|
||||
self::invalidateCache($tenantId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +94,9 @@ public function store(array $data): BankAccount
|
||||
'account_number' => $data['account_number'],
|
||||
'account_holder' => $data['account_holder'],
|
||||
'account_name' => $data['account_name'],
|
||||
'account_type' => $data['account_type'] ?? null,
|
||||
'balance' => $data['balance'] ?? 0,
|
||||
'currency' => $data['currency'] ?? 'KRW',
|
||||
'opened_at' => $data['opened_at'] ?? null,
|
||||
'branch_name' => $data['branch_name'] ?? null,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'status' => $data['status'] ?? 'active',
|
||||
'assigned_user_id' => $data['assigned_user_id'] ?? null,
|
||||
'is_primary' => $isPrimary,
|
||||
'sort_order' => $data['sort_order'] ?? 0,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
@@ -131,15 +124,8 @@ public function update(int $id, array $data): BankAccount
|
||||
'account_number' => $data['account_number'] ?? $account->account_number,
|
||||
'account_holder' => $data['account_holder'] ?? $account->account_holder,
|
||||
'account_name' => $data['account_name'] ?? $account->account_name,
|
||||
'account_type' => $data['account_type'] ?? $account->account_type,
|
||||
'balance' => $data['balance'] ?? $account->balance,
|
||||
'currency' => $data['currency'] ?? $account->currency,
|
||||
'opened_at' => $data['opened_at'] ?? $account->opened_at,
|
||||
'branch_name' => $data['branch_name'] ?? $account->branch_name,
|
||||
'memo' => $data['memo'] ?? $account->memo,
|
||||
'status' => $data['status'] ?? $account->status,
|
||||
'assigned_user_id' => $data['assigned_user_id'] ?? $account->assigned_user_id,
|
||||
'sort_order' => $data['sort_order'] ?? $account->sort_order,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,604 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Barobill;
|
||||
|
||||
use App\Models\Tenants\BarobillConfig;
|
||||
use App\Models\Tenants\BarobillMember;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use SoapClient;
|
||||
use SoapFault;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Throwable;
|
||||
|
||||
class BarobillSoapService extends Service
|
||||
{
|
||||
protected ?SoapClient $corpStateClient = null;
|
||||
|
||||
protected ?SoapClient $bankAccountClient = null;
|
||||
|
||||
protected ?SoapClient $cardClient = null;
|
||||
|
||||
protected string $certKey;
|
||||
|
||||
protected string $corpNum;
|
||||
|
||||
protected bool $isTestMode;
|
||||
|
||||
protected array $soapUrls;
|
||||
|
||||
protected array $errorMessages = [
|
||||
-11101 => '사업자번호가 설정되지 않았거나 유효하지 않습니다.',
|
||||
-11102 => 'CERTKEY가 유효하지 않습니다.',
|
||||
-11103 => '인증서가 만료되었거나 유효하지 않습니다.',
|
||||
-11104 => '해당 사업자가 등록되어 있지 않습니다.',
|
||||
-11105 => '이미 등록된 사업자입니다.',
|
||||
-11106 => '아이디가 이미 존재합니다.',
|
||||
-11201 => '필수 파라미터가 누락되었습니다.',
|
||||
-26001 => '공동인증서가 등록되어 있지 않습니다.',
|
||||
-32000 => '알 수 없는 오류가 발생했습니다.',
|
||||
-32001 => '사업자번호가 유효하지 않습니다.',
|
||||
-32002 => '아이디가 유효하지 않습니다.',
|
||||
-32003 => '비밀번호가 유효하지 않습니다.',
|
||||
-32004 => '상호명이 유효하지 않습니다.',
|
||||
-32005 => '대표자명이 유효하지 않습니다.',
|
||||
-32006 => '이메일 형식이 유효하지 않습니다.',
|
||||
-32010 => '이미 등록된 사업자번호입니다.',
|
||||
-32011 => '이미 등록된 아이디입니다.',
|
||||
-32012 => '이미 등록된 아이디입니다. 다른 아이디를 사용해주세요.',
|
||||
-32013 => '비밀번호 형식이 유효하지 않습니다. (영문/숫자/특수문자 조합 8자리 이상)',
|
||||
-32014 => '연락처 형식이 유효하지 않습니다.',
|
||||
-32020 => '파트너 사업자번호가 유효하지 않습니다.',
|
||||
-32021 => '파트너 인증키(CERTKEY)가 유효하지 않습니다.',
|
||||
-99999 => '서버 내부 오류가 발생했습니다.',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->isTestMode = config('services.barobill.test_mode', true);
|
||||
$this->initializeConfig();
|
||||
}
|
||||
|
||||
public function switchServerMode(bool $isTestMode): self
|
||||
{
|
||||
if ($this->isTestMode !== $isTestMode) {
|
||||
$this->isTestMode = $isTestMode;
|
||||
$this->corpStateClient = null;
|
||||
$this->bankAccountClient = null;
|
||||
$this->cardClient = null;
|
||||
$this->initializeConfig();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function initializeConfig(): void
|
||||
{
|
||||
$dbConfig = $this->loadConfigFromDatabase();
|
||||
|
||||
if ($dbConfig) {
|
||||
$this->certKey = $dbConfig->cert_key;
|
||||
$this->corpNum = $dbConfig->corp_num ?? '';
|
||||
$this->soapUrls = $this->buildSoapUrls($dbConfig->base_url);
|
||||
} else {
|
||||
$this->certKey = $this->isTestMode
|
||||
? config('services.barobill.cert_key_test', '')
|
||||
: config('services.barobill.cert_key_prod', '');
|
||||
$this->corpNum = config('services.barobill.corp_num', '');
|
||||
$baseUrl = $this->isTestMode
|
||||
? 'https://testws.baroservice.com'
|
||||
: 'https://ws.baroservice.com';
|
||||
$this->soapUrls = $this->buildSoapUrls($baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadConfigFromDatabase(): ?BarobillConfig
|
||||
{
|
||||
try {
|
||||
return BarobillConfig::getActive($this->isTestMode);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('바로빌 DB 설정 로드 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildSoapUrls(string $baseUrl): array
|
||||
{
|
||||
$baseUrl = rtrim($baseUrl, '/');
|
||||
|
||||
return [
|
||||
'corpstate' => $baseUrl.'/CORPSTATE.asmx?WSDL',
|
||||
'bankaccount' => $baseUrl.'/BANKACCOUNT.asmx?WSDL',
|
||||
'card' => $baseUrl.'/CARD.asmx?WSDL',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getCorpStateClient(): ?SoapClient
|
||||
{
|
||||
if ($this->corpStateClient === null) {
|
||||
try {
|
||||
$this->corpStateClient = new SoapClient($this->soapUrls['corpstate'], [
|
||||
'trace' => true,
|
||||
'encoding' => 'UTF-8',
|
||||
'exceptions' => true,
|
||||
'connection_timeout' => 30,
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('바로빌 CORPSTATE SOAP 클라이언트 생성 실패', [
|
||||
'error' => $e->getMessage(),
|
||||
'url' => $this->soapUrls['corpstate'],
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->corpStateClient;
|
||||
}
|
||||
|
||||
protected function getBankAccountClient(): ?SoapClient
|
||||
{
|
||||
if ($this->bankAccountClient === null) {
|
||||
try {
|
||||
$this->bankAccountClient = new SoapClient($this->soapUrls['bankaccount'], [
|
||||
'trace' => true,
|
||||
'encoding' => 'UTF-8',
|
||||
'exceptions' => true,
|
||||
'connection_timeout' => 30,
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('바로빌 BANKACCOUNT SOAP 클라이언트 생성 실패', [
|
||||
'error' => $e->getMessage(),
|
||||
'url' => $this->soapUrls['bankaccount'],
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->bankAccountClient;
|
||||
}
|
||||
|
||||
protected function getCardClient(): ?SoapClient
|
||||
{
|
||||
if ($this->cardClient === null) {
|
||||
try {
|
||||
$this->cardClient = new SoapClient($this->soapUrls['card'], [
|
||||
'trace' => true,
|
||||
'encoding' => 'UTF-8',
|
||||
'exceptions' => true,
|
||||
'connection_timeout' => 30,
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('바로빌 CARD SOAP 클라이언트 생성 실패', [
|
||||
'error' => $e->getMessage(),
|
||||
'url' => $this->soapUrls['card'],
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cardClient;
|
||||
}
|
||||
|
||||
protected function call(string $service, string $method, array $params = []): array
|
||||
{
|
||||
$client = match ($service) {
|
||||
'corpstate' => $this->getCorpStateClient(),
|
||||
'bankaccount' => $this->getBankAccountClient(),
|
||||
'card' => $this->getCardClient(),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $client) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => "SOAP 클라이언트가 초기화되지 않았습니다. ({$service})",
|
||||
'error_code' => -1,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($this->certKey)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'CERTKEY가 설정되지 않았습니다.',
|
||||
'error_code' => -2,
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($params['CERTKEY'])) {
|
||||
$params['CERTKEY'] = $this->certKey;
|
||||
}
|
||||
|
||||
try {
|
||||
Log::info('바로빌 SOAP API 호출', [
|
||||
'service' => $service,
|
||||
'method' => $method,
|
||||
'test_mode' => $this->isTestMode,
|
||||
]);
|
||||
|
||||
$result = $client->$method($params);
|
||||
$resultProperty = $method.'Result';
|
||||
|
||||
if (isset($result->$resultProperty)) {
|
||||
$resultData = $result->$resultProperty;
|
||||
|
||||
if (is_numeric($resultData) && $resultData < 0) {
|
||||
$errorMessage = $this->errorMessages[$resultData] ?? "바로빌 API 오류 코드: {$resultData}";
|
||||
|
||||
Log::error('바로빌 SOAP API 오류', [
|
||||
'method' => $method,
|
||||
'error_code' => $resultData,
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errorMessage,
|
||||
'error_code' => $resultData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $resultData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
];
|
||||
|
||||
} catch (SoapFault $e) {
|
||||
Log::error('바로빌 SOAP 오류', [
|
||||
'method' => $method,
|
||||
'fault_code' => $e->faultcode ?? null,
|
||||
'fault_string' => $e->faultstring ?? null,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'SOAP 오류: '.$e->getMessage(),
|
||||
'error_code' => $e->getCode(),
|
||||
];
|
||||
|
||||
} catch (Throwable $e) {
|
||||
Log::error('바로빌 SOAP API 호출 오류', [
|
||||
'method' => $method,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'API 호출 오류: '.$e->getMessage(),
|
||||
'error_code' => -999,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatBizNo(string $bizNo): string
|
||||
{
|
||||
return preg_replace('/[^0-9]/', '', $bizNo);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 회원사 조회
|
||||
// =========================================================================
|
||||
|
||||
protected function getMember(): BarobillMember
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$member = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||
|
||||
if (! $member) {
|
||||
throw new BadRequestHttpException(__('error.barobill.member_not_found'));
|
||||
}
|
||||
|
||||
$this->switchServerMode($member->isTestMode());
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SOAP 비즈니스 메서드
|
||||
// =========================================================================
|
||||
|
||||
public function registCorp(array $data): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($data['biz_no']),
|
||||
'CorpName' => $data['corp_name'],
|
||||
'CEOName' => $data['ceo_name'],
|
||||
'BizType' => $data['biz_type'] ?? '',
|
||||
'BizClass' => $data['biz_class'] ?? '',
|
||||
'PostNum' => $data['post_num'] ?? '',
|
||||
'Addr1' => $data['addr'] ?? '',
|
||||
'Addr2' => '',
|
||||
'MemberName' => $data['manager_name'] ?? '',
|
||||
'JuminNum' => '',
|
||||
'ID' => $data['barobill_id'],
|
||||
'PWD' => $data['barobill_pwd'],
|
||||
'Grade' => '2',
|
||||
'TEL' => $data['tel'] ?? '',
|
||||
'HP' => $data['manager_hp'] ?? '',
|
||||
'Email' => $data['manager_email'] ?? '',
|
||||
];
|
||||
|
||||
return $this->call('corpstate', 'RegistCorp', $params);
|
||||
}
|
||||
|
||||
public function getCorpState(string $corpNum): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
];
|
||||
|
||||
return $this->call('corpstate', 'GetCorpState', $params);
|
||||
}
|
||||
|
||||
public function getBankAccountScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
];
|
||||
|
||||
return $this->call('bankaccount', 'GetBankAccountScrapRequestURL', $params);
|
||||
}
|
||||
|
||||
public function getBankAccountLogUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
];
|
||||
|
||||
return $this->call('bankaccount', 'GetBankAccountLogURL', $params);
|
||||
}
|
||||
|
||||
public function getCertificateRegistUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
];
|
||||
|
||||
return $this->call('bankaccount', 'GetCertificateRegistURL', $params);
|
||||
}
|
||||
|
||||
public function getBankAccounts(string $corpNum, bool $availOnly = true): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'AvailOnly' => $availOnly,
|
||||
];
|
||||
|
||||
return $this->call('bankaccount', 'GetBankAccountEx', $params);
|
||||
}
|
||||
|
||||
public function getCards(string $corpNum, bool $availOnly = true): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'AvailOnly' => $availOnly ? 1 : 0,
|
||||
];
|
||||
|
||||
return $this->call('card', 'GetCardEx2', $params);
|
||||
}
|
||||
|
||||
public function getCardScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
];
|
||||
|
||||
return $this->call('card', 'GetCardScrapRequestURL', $params);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 컨트롤러 비즈니스 메서드
|
||||
// =========================================================================
|
||||
|
||||
public function registerLogin(array $data): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$barobillId = $data['barobill_id'];
|
||||
$password = $data['password'];
|
||||
|
||||
$member = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||
|
||||
if ($member) {
|
||||
$this->switchServerMode($member->isTestMode());
|
||||
}
|
||||
|
||||
$result = $this->getCorpState($member?->biz_no ?? config('services.barobill.corp_num'));
|
||||
|
||||
if (! $result['success']) {
|
||||
throw new BadRequestHttpException($result['error']);
|
||||
}
|
||||
|
||||
$member = BarobillMember::updateOrCreate(
|
||||
['tenant_id' => $tenantId],
|
||||
[
|
||||
'barobill_id' => $barobillId,
|
||||
'barobill_pwd' => $password,
|
||||
'status' => 'active',
|
||||
]
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $member->id,
|
||||
'barobill_id' => $member->barobill_id,
|
||||
'status' => $member->status,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerSignup(array $data): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$soapResult = $this->registCorp([
|
||||
'biz_no' => $data['business_number'],
|
||||
'corp_name' => $data['company_name'],
|
||||
'ceo_name' => $data['ceo_name'],
|
||||
'biz_type' => $data['business_type'] ?? '',
|
||||
'biz_class' => $data['business_category'] ?? '',
|
||||
'addr' => $data['address'] ?? '',
|
||||
'barobill_id' => $data['barobill_id'],
|
||||
'barobill_pwd' => $data['password'],
|
||||
'manager_name' => $data['manager_name'] ?? '',
|
||||
'manager_hp' => $data['manager_phone'] ?? '',
|
||||
'manager_email' => $data['manager_email'] ?? '',
|
||||
]);
|
||||
|
||||
if (! $soapResult['success']) {
|
||||
throw new BadRequestHttpException($soapResult['error']);
|
||||
}
|
||||
|
||||
$member = BarobillMember::updateOrCreate(
|
||||
['tenant_id' => $tenantId],
|
||||
[
|
||||
'biz_no' => $this->formatBizNo($data['business_number']),
|
||||
'corp_name' => $data['company_name'],
|
||||
'ceo_name' => $data['ceo_name'],
|
||||
'biz_type' => $data['business_type'] ?? '',
|
||||
'biz_class' => $data['business_category'] ?? '',
|
||||
'addr' => $data['address'] ?? '',
|
||||
'barobill_id' => $data['barobill_id'],
|
||||
'barobill_pwd' => $data['password'],
|
||||
'manager_name' => $data['manager_name'] ?? '',
|
||||
'manager_hp' => $data['manager_phone'] ?? '',
|
||||
'manager_email' => $data['manager_email'] ?? '',
|
||||
'status' => 'active',
|
||||
'server_mode' => $this->isTestMode ? 'test' : 'production',
|
||||
]
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $member->id,
|
||||
'barobill_id' => $member->barobill_id,
|
||||
'biz_no' => $member->formatted_biz_no,
|
||||
'status' => $member->status,
|
||||
];
|
||||
}
|
||||
|
||||
public function getBankServiceRedirectUrl(array $params): array
|
||||
{
|
||||
$member = $this->getMember();
|
||||
|
||||
$result = $this->getBankAccountLogUrl(
|
||||
$member->biz_no,
|
||||
$member->barobill_id,
|
||||
$member->barobill_pwd
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
throw new BadRequestHttpException($result['error']);
|
||||
}
|
||||
|
||||
return ['url' => $result['data']];
|
||||
}
|
||||
|
||||
public function getIntegrationStatus(): array
|
||||
{
|
||||
$member = $this->getMember();
|
||||
|
||||
$bankResult = $this->getBankAccounts($member->biz_no);
|
||||
$cardResult = $this->getCards($member->biz_no);
|
||||
|
||||
$bankCount = 0;
|
||||
if ($bankResult['success'] && isset($bankResult['data'])) {
|
||||
$data = $bankResult['data'];
|
||||
if (is_object($data) && isset($data->BankAccountInfo)) {
|
||||
$info = $data->BankAccountInfo;
|
||||
$bankCount = is_array($info) ? count($info) : 1;
|
||||
} elseif (is_array($data)) {
|
||||
$bankCount = count($data);
|
||||
}
|
||||
}
|
||||
|
||||
$cardCount = 0;
|
||||
if ($cardResult['success'] && isset($cardResult['data'])) {
|
||||
$data = $cardResult['data'];
|
||||
if (is_object($data) && isset($data->CardInfo)) {
|
||||
$info = $data->CardInfo;
|
||||
$cardCount = is_array($info) ? count($info) : 1;
|
||||
} elseif (is_array($data)) {
|
||||
$cardCount = count($data);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'bank_account_count' => $bankCount,
|
||||
'card_count' => $cardCount,
|
||||
'member' => [
|
||||
'barobill_id' => $member->barobill_id,
|
||||
'biz_no' => $member->formatted_biz_no,
|
||||
'status' => $member->status,
|
||||
'server_mode' => $member->server_mode,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccountLinkRedirectUrl(): array
|
||||
{
|
||||
$member = $this->getMember();
|
||||
|
||||
$result = $this->getBankAccountScrapRequestUrl(
|
||||
$member->biz_no,
|
||||
$member->barobill_id,
|
||||
$member->barobill_pwd
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
throw new BadRequestHttpException($result['error']);
|
||||
}
|
||||
|
||||
return ['url' => $result['data']];
|
||||
}
|
||||
|
||||
public function getCardLinkRedirectUrl(): array
|
||||
{
|
||||
$member = $this->getMember();
|
||||
|
||||
$result = $this->getCardScrapRequestUrl(
|
||||
$member->biz_no,
|
||||
$member->barobill_id,
|
||||
$member->barobill_pwd
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
throw new BadRequestHttpException($result['error']);
|
||||
}
|
||||
|
||||
return ['url' => $result['data']];
|
||||
}
|
||||
|
||||
public function getCertificateRedirectUrl(): array
|
||||
{
|
||||
$member = $this->getMember();
|
||||
|
||||
$result = $this->getCertificateRegistUrl(
|
||||
$member->biz_no,
|
||||
$member->barobill_id,
|
||||
$member->barobill_pwd
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
throw new BadRequestHttpException($result['error']);
|
||||
}
|
||||
|
||||
return ['url' => $result['data']];
|
||||
}
|
||||
}
|
||||
@@ -4,130 +4,37 @@
|
||||
|
||||
use App\Models\Tenants\BarobillSetting;
|
||||
use App\Models\Tenants\TaxInvoice;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use SoapClient;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* 바로빌 API 연동 서비스 (SOAP)
|
||||
* 바로빌 API 연동 서비스
|
||||
*
|
||||
* 바로빌 개발자센터: https://dev.barobill.co.kr/
|
||||
* 바로빌은 SOAP API만 제공하므로 SoapClient를 사용합니다.
|
||||
*/
|
||||
class BarobillService extends Service
|
||||
{
|
||||
/**
|
||||
* 바로빌 SOAP 기본 URL
|
||||
* 바로빌 API 기본 URL
|
||||
*/
|
||||
private const SOAP_BASE_URL = 'https://ws.baroservice.com';
|
||||
private const API_BASE_URL = 'https://ws.barobill.co.kr';
|
||||
|
||||
/**
|
||||
* 바로빌 SOAP 테스트 URL
|
||||
* 바로빌 API 테스트 URL
|
||||
*/
|
||||
private const SOAP_TEST_URL = 'https://testws.baroservice.com';
|
||||
private const API_TEST_URL = 'https://testws.barobill.co.kr';
|
||||
|
||||
/**
|
||||
* 테스트 모드 여부
|
||||
*/
|
||||
private bool $testMode;
|
||||
|
||||
/**
|
||||
* TI(Tax Invoice) SOAP 클라이언트
|
||||
*/
|
||||
private ?SoapClient $tiSoapClient = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->testMode = config('services.barobill.test_mode', true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SOAP 클라이언트
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* TI SOAP 클라이언트 초기화/반환
|
||||
*/
|
||||
private function getTiSoapClient(): SoapClient
|
||||
{
|
||||
if ($this->tiSoapClient === null) {
|
||||
$baseUrl = $this->testMode ? self::SOAP_TEST_URL : self::SOAP_BASE_URL;
|
||||
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'verify_peer' => ! $this->testMode,
|
||||
'verify_peer_name' => ! $this->testMode,
|
||||
'allow_self_signed' => $this->testMode,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->tiSoapClient = new SoapClient($baseUrl.'/TI.asmx?WSDL', [
|
||||
'trace' => true,
|
||||
'encoding' => 'UTF-8',
|
||||
'exceptions' => true,
|
||||
'connection_timeout' => 30,
|
||||
'stream_context' => $context,
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->tiSoapClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* SOAP API 호출
|
||||
*
|
||||
* MNG EtaxController::callBarobillSOAP() 포팅
|
||||
* 음수 반환값 = 에러 코드 (바로빌 규격)
|
||||
*/
|
||||
private function callSoap(string $method, array $params): array
|
||||
{
|
||||
$client = $this->getTiSoapClient();
|
||||
|
||||
if (! isset($params['CERTKEY'])) {
|
||||
$setting = $this->getSetting();
|
||||
if (! $setting) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '바로빌 설정이 없습니다.',
|
||||
];
|
||||
}
|
||||
$params['CERTKEY'] = $setting->cert_key;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $client->$method($params);
|
||||
$resultProperty = $method.'Result';
|
||||
|
||||
if (isset($result->$resultProperty)) {
|
||||
$resultData = $result->$resultProperty;
|
||||
|
||||
// 바로빌 규격: 음수 반환값은 에러 코드
|
||||
if (is_numeric($resultData) && $resultData < 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '바로빌 API 오류 코드: '.$resultData,
|
||||
'error_code' => (int) $resultData,
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => true, 'data' => $resultData];
|
||||
}
|
||||
|
||||
return ['success' => true, 'data' => $result];
|
||||
} catch (\SoapFault $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'SOAP 오류: '.$e->getMessage(),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'API 호출 오류: '.$e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 설정 관리
|
||||
// =========================================================================
|
||||
@@ -171,7 +78,7 @@ public function saveSetting(array $data): BarobillSetting
|
||||
}
|
||||
|
||||
/**
|
||||
* 연동 테스트 (SOAP)
|
||||
* 연동 테스트
|
||||
*/
|
||||
public function testConnection(): array
|
||||
{
|
||||
@@ -182,17 +89,15 @@ public function testConnection(): array
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->callSoap('GetAccessToken', [
|
||||
// 바로빌 API 토큰 조회로 연동 테스트
|
||||
$response = $this->callApi('GetAccessToken', [
|
||||
'CERTKEY' => $setting->cert_key,
|
||||
'CorpNum' => $setting->corp_num,
|
||||
'ID' => $setting->barobill_id,
|
||||
]);
|
||||
|
||||
if ($response['success']) {
|
||||
$resultData = $response['data'];
|
||||
|
||||
// 양수 또는 문자열 토큰 = 성공
|
||||
if (is_string($resultData) || (is_numeric($resultData) && $resultData > 0)) {
|
||||
if (! empty($response['AccessToken'])) {
|
||||
// 검증 성공 시 verified_at 업데이트
|
||||
$setting->verified_at = now();
|
||||
$setting->save();
|
||||
|
||||
@@ -202,11 +107,8 @@ public function testConnection(): array
|
||||
'verified_at' => $setting->verified_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception($response['error'] ?? __('error.barobill.connection_failed'));
|
||||
} catch (BadRequestHttpException $e) {
|
||||
throw $e;
|
||||
throw new BadRequestHttpException($response['Message'] ?? __('error.barobill.connection_failed'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('바로빌 연동 테스트 실패', [
|
||||
'tenant_id' => $this->tenantId(),
|
||||
@@ -259,29 +161,52 @@ public function checkBusinessNumber(string $businessNumber): array
|
||||
];
|
||||
}
|
||||
|
||||
// 바로빌 SOAP API 조회 시도
|
||||
// 바로빌 API 조회 시도
|
||||
try {
|
||||
$response = $this->callSoap('CheckCorpNum', [
|
||||
$response = $this->callApi('CheckCorpNum', [
|
||||
'CorpNum' => $businessNumber,
|
||||
]);
|
||||
|
||||
if ($response['success']) {
|
||||
$resultData = $response['data'];
|
||||
// 바로빌 응답 해석
|
||||
if (isset($response['CorpState'])) {
|
||||
$state = $response['CorpState'];
|
||||
$isValid = in_array($state, ['01', '02']); // 01: 사업중, 02: 휴업
|
||||
$statusLabel = match ($state) {
|
||||
'01' => '사업중',
|
||||
'02' => '휴업',
|
||||
'03' => '폐업',
|
||||
default => '조회 불가',
|
||||
};
|
||||
|
||||
// 양수 결과 = 유효한 사업자
|
||||
if (is_numeric($resultData) && $resultData >= 0) {
|
||||
return [
|
||||
'valid' => true,
|
||||
'status' => 'active',
|
||||
'status_label' => '유효함',
|
||||
'corp_name' => null,
|
||||
'ceo_name' => null,
|
||||
'message' => __('message.company.business_number_valid'),
|
||||
'valid' => $isValid,
|
||||
'status' => $state,
|
||||
'status_label' => $statusLabel,
|
||||
'corp_name' => $response['CorpName'] ?? null,
|
||||
'ceo_name' => $response['CEOName'] ?? null,
|
||||
'message' => $isValid
|
||||
? __('message.company.business_number_valid')
|
||||
: __('error.company.business_closed'),
|
||||
];
|
||||
}
|
||||
|
||||
// 응답 형식이 다른 경우 (결과 코드 방식)
|
||||
if (isset($response['Result'])) {
|
||||
$isValid = $response['Result'] >= 0;
|
||||
|
||||
return [
|
||||
'valid' => $isValid,
|
||||
'status' => $isValid ? 'active' : 'unknown',
|
||||
'status_label' => $isValid ? '유효함' : '조회 불가',
|
||||
'corp_name' => $response['CorpName'] ?? null,
|
||||
'ceo_name' => $response['CEOName'] ?? null,
|
||||
'message' => $isValid
|
||||
? __('message.company.business_number_valid')
|
||||
: ($response['Message'] ?? __('error.company.check_failed')),
|
||||
];
|
||||
}
|
||||
|
||||
// API 실패 시 형식 검증 결과만 반환
|
||||
// 기본 응답 (체크섬만 통과한 경우)
|
||||
return [
|
||||
'valid' => true,
|
||||
'status' => 'format_valid',
|
||||
@@ -341,9 +266,7 @@ private function validateBusinessNumberChecksum(string $businessNumber): bool
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 세금계산서 발행 (SOAP RegistAndIssueTaxInvoice)
|
||||
*
|
||||
* MNG EtaxController::issueTaxInvoice() 포팅
|
||||
* 세금계산서 발행
|
||||
*/
|
||||
public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
||||
{
|
||||
@@ -354,13 +277,16 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
||||
}
|
||||
|
||||
try {
|
||||
// 바로빌 API 호출을 위한 데이터 구성
|
||||
$apiData = $this->buildTaxInvoiceData($taxInvoice, $setting);
|
||||
$response = $this->callSoap('RegistAndIssueTaxInvoice', $apiData);
|
||||
|
||||
if ($response['success']) {
|
||||
$resultData = $response['data'];
|
||||
// 바로빌 규격: 양수 반환값이 Invoice ID
|
||||
$taxInvoice->barobill_invoice_id = is_numeric($resultData) ? (string) $resultData : null;
|
||||
// 세금계산서 발행 API 호출
|
||||
$response = $this->callApi('RegistAndIssueTaxInvoice', $apiData);
|
||||
|
||||
if (! empty($response['InvoiceID'])) {
|
||||
// 발행 성공
|
||||
$taxInvoice->barobill_invoice_id = $response['InvoiceID'];
|
||||
$taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? null;
|
||||
$taxInvoice->status = TaxInvoice::STATUS_ISSUED;
|
||||
$taxInvoice->issued_at = now();
|
||||
$taxInvoice->error_message = null;
|
||||
@@ -369,15 +295,13 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
||||
Log::info('세금계산서 발행 성공', [
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'tax_invoice_id' => $taxInvoice->id,
|
||||
'barobill_invoice_id' => $taxInvoice->barobill_invoice_id,
|
||||
'barobill_invoice_id' => $response['InvoiceID'],
|
||||
]);
|
||||
|
||||
return $taxInvoice->fresh();
|
||||
}
|
||||
|
||||
throw new \Exception($response['error'] ?? '발행 실패');
|
||||
} catch (BadRequestHttpException $e) {
|
||||
throw $e;
|
||||
throw new \Exception($response['Message'] ?? '발행 실패');
|
||||
} catch (\Exception $e) {
|
||||
// 발행 실패
|
||||
$taxInvoice->status = TaxInvoice::STATUS_FAILED;
|
||||
@@ -395,7 +319,7 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 취소 (SOAP CancelTaxInvoice)
|
||||
* 세금계산서 취소
|
||||
*/
|
||||
public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInvoice
|
||||
{
|
||||
@@ -410,15 +334,16 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->callSoap('ProcTaxInvoice', [
|
||||
// 세금계산서 취소 API 호출
|
||||
$response = $this->callApi('CancelTaxInvoice', [
|
||||
'CERTKEY' => $setting->cert_key,
|
||||
'CorpNum' => $setting->corp_num,
|
||||
'MgtNum' => $taxInvoice->barobill_invoice_id,
|
||||
'ProcType' => 4, // 4: 발행취소
|
||||
'ID' => $setting->barobill_id,
|
||||
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
||||
'Memo' => $reason,
|
||||
]);
|
||||
|
||||
if ($response['success']) {
|
||||
if ($response['Result'] === 0 || ! empty($response['Success'])) {
|
||||
$taxInvoice->status = TaxInvoice::STATUS_CANCELLED;
|
||||
$taxInvoice->cancelled_at = now();
|
||||
$taxInvoice->description = ($taxInvoice->description ? $taxInvoice->description."\n" : '').'취소 사유: '.$reason;
|
||||
@@ -433,9 +358,7 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv
|
||||
return $taxInvoice->fresh();
|
||||
}
|
||||
|
||||
throw new \Exception($response['error'] ?? '취소 실패');
|
||||
} catch (BadRequestHttpException $e) {
|
||||
throw $e;
|
||||
throw new \Exception($response['Message'] ?? '취소 실패');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('세금계산서 취소 실패', [
|
||||
'tenant_id' => $this->tenantId(),
|
||||
@@ -448,7 +371,7 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv
|
||||
}
|
||||
|
||||
/**
|
||||
* 국세청 전송 상태 조회 (SOAP GetTaxInvoiceState)
|
||||
* 국세청 전송 상태 조회
|
||||
*/
|
||||
public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice
|
||||
{
|
||||
@@ -463,38 +386,27 @@ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->callSoap('GetTaxInvoiceState', [
|
||||
$response = $this->callApi('GetTaxInvoiceState', [
|
||||
'CERTKEY' => $setting->cert_key,
|
||||
'CorpNum' => $setting->corp_num,
|
||||
'MgtNum' => $taxInvoice->barobill_invoice_id,
|
||||
'ID' => $setting->barobill_id,
|
||||
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
||||
]);
|
||||
|
||||
if ($response['success'] && $response['data']) {
|
||||
$stateData = $response['data'];
|
||||
|
||||
// SOAP 결과 객체에서 상태 추출
|
||||
$state = is_object($stateData) ? ($stateData->NTSSendState ?? null) : null;
|
||||
|
||||
if ($state !== null) {
|
||||
$taxInvoice->nts_send_status = (string) $state;
|
||||
if (! empty($response['State'])) {
|
||||
$taxInvoice->nts_send_status = $response['State'];
|
||||
|
||||
// 국세청 전송 완료 시 상태 업데이트
|
||||
if (in_array($state, [3, '3', '전송완료']) && ! $taxInvoice->sent_at) {
|
||||
if ($response['State'] === '전송완료' && ! $taxInvoice->sent_at) {
|
||||
$taxInvoice->status = TaxInvoice::STATUS_SENT;
|
||||
$taxInvoice->sent_at = now();
|
||||
|
||||
if (is_object($stateData) && ! empty($stateData->NTSConfirmNum)) {
|
||||
$taxInvoice->nts_confirm_num = $stateData->NTSConfirmNum;
|
||||
}
|
||||
$taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? $taxInvoice->nts_confirm_num;
|
||||
}
|
||||
|
||||
$taxInvoice->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $taxInvoice->fresh();
|
||||
} catch (BadRequestHttpException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('국세청 전송 상태 조회 실패', [
|
||||
'tenant_id' => $this->tenantId(),
|
||||
@@ -511,123 +423,126 @@ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 세금계산서 발행용 데이터 구성 (MNG SOAP 형식)
|
||||
*
|
||||
* MNG EtaxController::issueTaxInvoice() 의 Invoice 구조를 포팅
|
||||
* InvoicerParty/InvoiceeParty/TaxInvoiceTradeLineItems 중첩 구조
|
||||
* 바로빌 API 호출
|
||||
*/
|
||||
private function callApi(string $method, array $data): array
|
||||
{
|
||||
$baseUrl = $this->testMode ? self::API_TEST_URL : self::API_BASE_URL;
|
||||
$url = $baseUrl.'/TI/'.$method;
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post($url, $data);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('API 호출 실패: '.$response->status());
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 발행용 데이터 구성
|
||||
*/
|
||||
private function buildTaxInvoiceData(TaxInvoice $taxInvoice, BarobillSetting $setting): array
|
||||
{
|
||||
$supplyAmt = (int) $taxInvoice->supply_amount;
|
||||
$taxAmt = (int) $taxInvoice->tax_amount;
|
||||
$total = (int) $taxInvoice->total_amount;
|
||||
$taxType = $taxAmt == 0 ? 2 : 1; // 1:과세, 2:영세, 3:면세
|
||||
|
||||
// 관리번호 (유니크)
|
||||
$mgtNum = 'SAM'.date('YmdHis').$taxInvoice->id;
|
||||
|
||||
// 품목 구성
|
||||
$tradeLineItems = [];
|
||||
foreach ($taxInvoice->items ?? [] as $item) {
|
||||
$tradeLineItems[] = [
|
||||
'PurchaseExpiry' => $taxInvoice->issue_date->format('Ymd'),
|
||||
'Name' => $item['name'] ?? '',
|
||||
'Information' => $item['spec'] ?? '',
|
||||
'ChargeableUnit' => (string) ($item['qty'] ?? 1),
|
||||
'UnitPrice' => (string) ($item['unit_price'] ?? 0),
|
||||
'Amount' => (string) ($item['supply_amt'] ?? 0),
|
||||
'Tax' => (string) ($item['tax_amt'] ?? 0),
|
||||
'Description' => $item['remark'] ?? '',
|
||||
// 품목 데이터 구성
|
||||
$items = [];
|
||||
foreach ($taxInvoice->items ?? [] as $index => $item) {
|
||||
$items[] = [
|
||||
'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'),
|
||||
'ItemName' => $item['name'] ?? '',
|
||||
'Spec' => $item['spec'] ?? '',
|
||||
'Qty' => $item['qty'] ?? 1,
|
||||
'UnitCost' => $item['unit_price'] ?? 0,
|
||||
'SupplyCost' => $item['supply_amt'] ?? 0,
|
||||
'Tax' => $item['tax_amt'] ?? 0,
|
||||
'Remark' => $item['remark'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// 품목이 없는 경우 기본 품목 추가
|
||||
if (empty($tradeLineItems)) {
|
||||
$tradeLineItems[] = [
|
||||
'PurchaseExpiry' => $taxInvoice->issue_date->format('Ymd'),
|
||||
'Name' => $taxInvoice->description ?? '품목',
|
||||
'Information' => '',
|
||||
'ChargeableUnit' => '1',
|
||||
'UnitPrice' => (string) $supplyAmt,
|
||||
'Amount' => (string) $supplyAmt,
|
||||
'Tax' => (string) $taxAmt,
|
||||
'Description' => '',
|
||||
if (empty($items)) {
|
||||
$items[] = [
|
||||
'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'),
|
||||
'ItemName' => $taxInvoice->description ?? '품목',
|
||||
'Spec' => '',
|
||||
'Qty' => 1,
|
||||
'UnitCost' => (float) $taxInvoice->supply_amount,
|
||||
'SupplyCost' => (float) $taxInvoice->supply_amount,
|
||||
'Tax' => (float) $taxInvoice->tax_amount,
|
||||
'Remark' => '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'CERTKEY' => $setting->cert_key,
|
||||
'CorpNum' => $setting->corp_num,
|
||||
'Invoice' => [
|
||||
'IssueDirection' => 1, // 1: 정발행
|
||||
'TaxInvoiceType' => $this->mapInvoiceTypeToCode($taxInvoice->invoice_type),
|
||||
'ModifyCode' => '',
|
||||
'TaxType' => $taxType,
|
||||
'TaxCalcType' => 1, // 1: 소계합계
|
||||
'PurposeType' => 2, // 2: 청구
|
||||
'ID' => $setting->barobill_id,
|
||||
'TaxInvoice' => [
|
||||
'InvoiceType' => $this->mapInvoiceType($taxInvoice->invoice_type),
|
||||
'IssueType' => $this->mapIssueType($taxInvoice->issue_type),
|
||||
'TaxType' => '과세',
|
||||
'PurposeType' => '영수',
|
||||
'WriteDate' => $taxInvoice->issue_date->format('Ymd'),
|
||||
'AmountTotal' => (string) $supplyAmt,
|
||||
'TaxTotal' => (string) $taxAmt,
|
||||
'TotalAmount' => (string) $total,
|
||||
'Cash' => '0',
|
||||
'ChkBill' => '0',
|
||||
'Note' => '0',
|
||||
'Credit' => (string) $total,
|
||||
|
||||
// 공급자 정보
|
||||
'InvoicerCorpNum' => $taxInvoice->supplier_corp_num,
|
||||
'InvoicerCorpName' => $taxInvoice->supplier_corp_name,
|
||||
'InvoicerCEOName' => $taxInvoice->supplier_ceo_name,
|
||||
'InvoicerAddr' => $taxInvoice->supplier_addr,
|
||||
'InvoicerBizType' => $taxInvoice->supplier_biz_type,
|
||||
'InvoicerBizClass' => $taxInvoice->supplier_biz_class,
|
||||
'InvoicerContactID' => $taxInvoice->supplier_contact_id,
|
||||
|
||||
// 공급받는자 정보
|
||||
'InvoiceeCorpNum' => $taxInvoice->buyer_corp_num,
|
||||
'InvoiceeCorpName' => $taxInvoice->buyer_corp_name,
|
||||
'InvoiceeCEOName' => $taxInvoice->buyer_ceo_name,
|
||||
'InvoiceeAddr' => $taxInvoice->buyer_addr,
|
||||
'InvoiceeBizType' => $taxInvoice->buyer_biz_type,
|
||||
'InvoiceeBizClass' => $taxInvoice->buyer_biz_class,
|
||||
'InvoiceeContactID' => $taxInvoice->buyer_contact_id,
|
||||
|
||||
// 금액 정보
|
||||
'SupplyCostTotal' => (int) $taxInvoice->supply_amount,
|
||||
'TaxTotal' => (int) $taxInvoice->tax_amount,
|
||||
'TotalAmount' => (int) $taxInvoice->total_amount,
|
||||
|
||||
// 품목 정보
|
||||
'TaxInvoiceTradeLineItems' => $items,
|
||||
|
||||
// 비고
|
||||
'Remark1' => $taxInvoice->description ?? '',
|
||||
'Remark2' => '',
|
||||
'Remark3' => '',
|
||||
'InvoicerParty' => [
|
||||
'MgtNum' => $mgtNum,
|
||||
'CorpNum' => $taxInvoice->supplier_corp_num,
|
||||
'TaxRegID' => '',
|
||||
'CorpName' => $taxInvoice->supplier_corp_name,
|
||||
'CEOName' => $taxInvoice->supplier_ceo_name ?? '',
|
||||
'Addr' => $taxInvoice->supplier_addr ?? '',
|
||||
'BizType' => $taxInvoice->supplier_biz_type ?? '',
|
||||
'BizClass' => $taxInvoice->supplier_biz_class ?? '',
|
||||
'ContactID' => $setting->barobill_id,
|
||||
'ContactName' => $setting->contact_name ?? '',
|
||||
'TEL' => $setting->contact_tel ?? '',
|
||||
'HP' => '',
|
||||
'Email' => $setting->contact_id ?? '',
|
||||
],
|
||||
'InvoiceeParty' => [
|
||||
'MgtNum' => '',
|
||||
'CorpNum' => str_replace('-', '', $taxInvoice->buyer_corp_num ?? ''),
|
||||
'TaxRegID' => '',
|
||||
'CorpName' => $taxInvoice->buyer_corp_name ?? '',
|
||||
'CEOName' => $taxInvoice->buyer_ceo_name ?? '',
|
||||
'Addr' => $taxInvoice->buyer_addr ?? '',
|
||||
'BizType' => $taxInvoice->buyer_biz_type ?? '',
|
||||
'BizClass' => $taxInvoice->buyer_biz_class ?? '',
|
||||
'ContactID' => '',
|
||||
'ContactName' => '',
|
||||
'TEL' => '',
|
||||
'HP' => '',
|
||||
'Email' => $taxInvoice->buyer_contact_id ?? '',
|
||||
],
|
||||
'BrokerParty' => [],
|
||||
'TaxInvoiceTradeLineItems' => [
|
||||
'TaxInvoiceTradeLineItem' => $tradeLineItems,
|
||||
],
|
||||
],
|
||||
'SendSMS' => false,
|
||||
'ForceIssue' => false,
|
||||
'MailTitle' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 유형을 바로빌 코드로 매핑
|
||||
*
|
||||
* @return int 1: 세금계산서, 2: 계산서, 4: 수정세금계산서
|
||||
* 세금계산서 유형 매핑
|
||||
*/
|
||||
private function mapInvoiceTypeToCode(string $type): int
|
||||
private function mapInvoiceType(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
TaxInvoice::TYPE_TAX_INVOICE => 1,
|
||||
TaxInvoice::TYPE_INVOICE => 2,
|
||||
TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => 4,
|
||||
default => 1,
|
||||
TaxInvoice::TYPE_TAX_INVOICE => '세금계산서',
|
||||
TaxInvoice::TYPE_INVOICE => '계산서',
|
||||
TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서',
|
||||
default => '세금계산서',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 발행 유형 매핑
|
||||
*/
|
||||
private function mapIssueType(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
TaxInvoice::ISSUE_TYPE_NORMAL => '정발행',
|
||||
TaxInvoice::ISSUE_TYPE_REVERSE => '역발행',
|
||||
TaxInvoice::ISSUE_TYPE_TRUSTEE => '위수탁',
|
||||
default => '정발행',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,19 +87,11 @@ public function store(array $data): Card
|
||||
$card = new Card;
|
||||
$card->tenant_id = $tenantId;
|
||||
$card->card_company = $data['card_company'];
|
||||
$card->card_type = $data['card_type'] ?? null;
|
||||
$card->setCardNumber($data['card_number']);
|
||||
$card->expiry_date = $data['expiry_date'];
|
||||
$card->card_name = $data['card_name'];
|
||||
$card->alias = $data['alias'] ?? null;
|
||||
$card->payment_day = $data['payment_day'] ?? null;
|
||||
$card->total_limit = $data['total_limit'] ?? null;
|
||||
$card->used_amount = $data['used_amount'] ?? null;
|
||||
$card->remaining_limit = $data['remaining_limit'] ?? null;
|
||||
$card->status = $data['status'] ?? 'active';
|
||||
$card->is_manual = $data['is_manual'] ?? false;
|
||||
$card->assigned_user_id = $data['assigned_user_id'] ?? null;
|
||||
$card->memo = $data['memo'] ?? null;
|
||||
$card->created_by = $userId;
|
||||
$card->updated_by = $userId;
|
||||
|
||||
@@ -107,13 +99,9 @@ public function store(array $data): Card
|
||||
$card->setCardPassword($data['card_password']);
|
||||
}
|
||||
|
||||
if (array_key_exists('csv', $data)) {
|
||||
$card->setCvc($data['csv']);
|
||||
}
|
||||
|
||||
$card->save();
|
||||
|
||||
return $this->show($card->id);
|
||||
return $card;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,9 +121,6 @@ public function update(int $id, array $data): Card
|
||||
if (isset($data['card_company'])) {
|
||||
$card->card_company = $data['card_company'];
|
||||
}
|
||||
if (array_key_exists('card_type', $data)) {
|
||||
$card->card_type = $data['card_type'];
|
||||
}
|
||||
if (isset($data['card_number'])) {
|
||||
$card->setCardNumber($data['card_number']);
|
||||
}
|
||||
@@ -145,36 +130,12 @@ public function update(int $id, array $data): Card
|
||||
if (isset($data['card_name'])) {
|
||||
$card->card_name = $data['card_name'];
|
||||
}
|
||||
if (array_key_exists('alias', $data)) {
|
||||
$card->alias = $data['alias'];
|
||||
}
|
||||
if (array_key_exists('csv', $data)) {
|
||||
$card->setCvc($data['csv']);
|
||||
}
|
||||
if (array_key_exists('payment_day', $data)) {
|
||||
$card->payment_day = $data['payment_day'];
|
||||
}
|
||||
if (array_key_exists('total_limit', $data)) {
|
||||
$card->total_limit = $data['total_limit'];
|
||||
}
|
||||
if (array_key_exists('used_amount', $data)) {
|
||||
$card->used_amount = $data['used_amount'];
|
||||
}
|
||||
if (array_key_exists('remaining_limit', $data)) {
|
||||
$card->remaining_limit = $data['remaining_limit'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$card->status = $data['status'];
|
||||
}
|
||||
if (array_key_exists('is_manual', $data)) {
|
||||
$card->is_manual = $data['is_manual'];
|
||||
}
|
||||
if (array_key_exists('assigned_user_id', $data)) {
|
||||
$card->assigned_user_id = $data['assigned_user_id'];
|
||||
}
|
||||
if (array_key_exists('memo', $data)) {
|
||||
$card->memo = $data['memo'];
|
||||
}
|
||||
if (isset($data['card_password'])) {
|
||||
$card->setCardPassword($data['card_password']);
|
||||
}
|
||||
@@ -182,7 +143,7 @@ public function update(int $id, array $data): Card
|
||||
$card->updated_by = $userId;
|
||||
$card->save();
|
||||
|
||||
return $this->show($card->id);
|
||||
return $card->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\CorporateCard;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CorporateCardService extends Service
|
||||
{
|
||||
/**
|
||||
* 법인카드 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = CorporateCard::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('card_name', 'like', "%{$search}%")
|
||||
->orWhere('card_company', 'like', "%{$search}%")
|
||||
->orWhere('card_number', 'like', "%{$search}%")
|
||||
->orWhere('card_holder_name', 'like', "%{$search}%")
|
||||
->orWhere('actual_user', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 카드 유형 필터
|
||||
if (! empty($params['card_type'])) {
|
||||
$query->where('card_type', $params['card_type']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$query->orderBy($params['sort_by'] ?? 'created_at', $params['sort_dir'] ?? 'desc');
|
||||
|
||||
return $query->paginate($params['per_page'] ?? 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 상세 조회
|
||||
*/
|
||||
public function show(int $id): CorporateCard
|
||||
{
|
||||
return CorporateCard::query()
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 등록
|
||||
*/
|
||||
public function store(array $data): CorporateCard
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId) {
|
||||
return CorporateCard::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'card_name' => $data['card_name'],
|
||||
'card_company' => $data['card_company'],
|
||||
'card_number' => $data['card_number'],
|
||||
'card_type' => $data['card_type'],
|
||||
'payment_day' => $data['payment_day'] ?? 15,
|
||||
'credit_limit' => $data['credit_limit'] ?? 0,
|
||||
'current_usage' => 0,
|
||||
'card_holder_name' => $data['card_holder_name'],
|
||||
'actual_user' => $data['actual_user'],
|
||||
'expiry_date' => $data['expiry_date'] ?? null,
|
||||
'cvc' => $data['cvc'] ?? null,
|
||||
'status' => $data['status'] ?? 'active',
|
||||
'memo' => $data['memo'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 수정
|
||||
*/
|
||||
public function update(int $id, array $data): CorporateCard
|
||||
{
|
||||
return DB::transaction(function () use ($id, $data) {
|
||||
$card = CorporateCard::query()
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
|
||||
$card->fill([
|
||||
'card_name' => $data['card_name'] ?? $card->card_name,
|
||||
'card_company' => $data['card_company'] ?? $card->card_company,
|
||||
'card_number' => $data['card_number'] ?? $card->card_number,
|
||||
'card_type' => $data['card_type'] ?? $card->card_type,
|
||||
'payment_day' => $data['payment_day'] ?? $card->payment_day,
|
||||
'credit_limit' => $data['credit_limit'] ?? $card->credit_limit,
|
||||
'card_holder_name' => $data['card_holder_name'] ?? $card->card_holder_name,
|
||||
'actual_user' => $data['actual_user'] ?? $card->actual_user,
|
||||
'expiry_date' => $data['expiry_date'] ?? $card->expiry_date,
|
||||
'cvc' => $data['cvc'] ?? $card->cvc,
|
||||
'status' => $data['status'] ?? $card->status,
|
||||
'memo' => $data['memo'] ?? $card->memo,
|
||||
]);
|
||||
|
||||
$card->save();
|
||||
|
||||
return $card->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 삭제
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$card = CorporateCard::query()
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
|
||||
$card->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인카드 상태 토글 (사용/정지)
|
||||
*/
|
||||
public function toggleStatus(int $id): CorporateCard
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$card = CorporateCard::query()
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
|
||||
$card->toggleStatus();
|
||||
$card->save();
|
||||
|
||||
return $card;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 법인카드 목록 (셀렉트박스용)
|
||||
*/
|
||||
public function getActiveCards(): array
|
||||
{
|
||||
return CorporateCard::query()
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->where('status', 'active')
|
||||
->orderBy('card_name')
|
||||
->get(['id', 'card_name', 'card_company', 'card_number', 'card_type'])
|
||||
->map(function ($card) {
|
||||
return [
|
||||
'id' => $card->id,
|
||||
'card_name' => $card->card_name,
|
||||
'card_company' => $card->card_company,
|
||||
'display_number' => $card->getMaskedCardNumber(),
|
||||
'card_type' => $card->card_type,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\TaxInvoice;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -272,105 +271,6 @@ public function checkStatus(int $id): TaxInvoice
|
||||
return $this->barobillService->checkNtsSendStatus($taxInvoice);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 공급자 설정
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 공급자 설정 조회 (BarobillSetting 기반, 미설정 시 Tenant 정보 Fallback)
|
||||
*/
|
||||
public function getSupplierSettings(): array
|
||||
{
|
||||
$setting = $this->barobillService->getSetting();
|
||||
|
||||
if ($setting && $this->hasSupplierData($setting)) {
|
||||
return [
|
||||
'business_number' => $setting->corp_num,
|
||||
'company_name' => $setting->corp_name,
|
||||
'representative_name' => $setting->ceo_name,
|
||||
'address' => $setting->addr,
|
||||
'business_type' => $setting->biz_type,
|
||||
'business_item' => $setting->biz_class,
|
||||
'contact_name' => $setting->contact_name,
|
||||
'contact_phone' => $setting->contact_tel,
|
||||
'contact_email' => $setting->contact_id,
|
||||
];
|
||||
}
|
||||
|
||||
// BarobillSetting 미설정 시 Tenant 정보를 기본값으로 반환
|
||||
return $this->getSupplierSettingsFromTenant();
|
||||
}
|
||||
|
||||
/**
|
||||
* BarobillSetting에 공급자 정보가 입력되어 있는지 확인
|
||||
*/
|
||||
private function hasSupplierData($setting): bool
|
||||
{
|
||||
return ! empty($setting->corp_num) || ! empty($setting->corp_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant 정보에서 공급자 설정 기본값 조회
|
||||
*/
|
||||
private function getSupplierSettingsFromTenant(): array
|
||||
{
|
||||
$tenant = Tenant::find($this->tenantId());
|
||||
|
||||
if (! $tenant) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = $tenant->options ?? [];
|
||||
|
||||
return [
|
||||
'business_number' => $tenant->business_num ?? '',
|
||||
'company_name' => $tenant->company_name ?? '',
|
||||
'representative_name' => $tenant->ceo_name ?? '',
|
||||
'address' => $tenant->address ?? '',
|
||||
'business_type' => $options['business_type'] ?? '',
|
||||
'business_item' => $options['business_category'] ?? '',
|
||||
'contact_name' => $options['tax_invoice_contact'] ?? '',
|
||||
'contact_phone' => $tenant->phone ?? '',
|
||||
'contact_email' => $options['tax_invoice_email'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 공급자 설정 저장
|
||||
*/
|
||||
public function saveSupplierSettings(array $data): array
|
||||
{
|
||||
$this->barobillService->saveSetting([
|
||||
'corp_num' => $data['business_number'] ?? null,
|
||||
'corp_name' => $data['company_name'] ?? null,
|
||||
'ceo_name' => $data['representative_name'] ?? null,
|
||||
'addr' => $data['address'] ?? null,
|
||||
'biz_type' => $data['business_type'] ?? null,
|
||||
'biz_class' => $data['business_item'] ?? null,
|
||||
'contact_name' => $data['contact_name'] ?? null,
|
||||
'contact_tel' => $data['contact_phone'] ?? null,
|
||||
'contact_id' => $data['contact_email'] ?? null,
|
||||
]);
|
||||
|
||||
return $this->getSupplierSettings();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 생성+발행 통합
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 세금계산서 생성 후 즉시 발행
|
||||
*/
|
||||
public function createAndIssue(array $data): TaxInvoice
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$taxInvoice = $this->create($data);
|
||||
|
||||
return $this->barobillService->issueTaxInvoice($taxInvoice);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 통계
|
||||
// =========================================================================
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Role", description="역할 관리(목록/조회/등록/수정/삭제/통계/활성)")
|
||||
* @OA\Tag(name="Role", description="역할 관리(목록/조회/등록/수정/삭제)")
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -18,9 +18,6 @@
|
||||
* @OA\Property(property="name", type="string", example="menu-manager"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="메뉴 관리 역할"),
|
||||
* @OA\Property(property="guard_name", type="string", example="api"),
|
||||
* @OA\Property(property="is_hidden", type="boolean", example=false),
|
||||
* @OA\Property(property="permissions_count", type="integer", example=12),
|
||||
* @OA\Property(property="users_count", type="integer", example=3),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-08-16 10:00:00"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-08-16 10:00:00")
|
||||
* )
|
||||
@@ -50,8 +47,7 @@
|
||||
* required={"name"},
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="menu-manager", description="역할명(테넌트+가드 내 고유)"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="메뉴 관리 역할"),
|
||||
* @OA\Property(property="is_hidden", type="boolean", example=false, description="숨김 여부")
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="메뉴 관리 역할")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
@@ -59,19 +55,7 @@
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="name", type="string", example="menu-admin"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="설명 변경"),
|
||||
* @OA\Property(property="is_hidden", type="boolean", example=false)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="RoleStats",
|
||||
* type="object",
|
||||
* description="역할 통계",
|
||||
*
|
||||
* @OA\Property(property="total", type="integer", example=5),
|
||||
* @OA\Property(property="visible", type="integer", example=3),
|
||||
* @OA\Property(property="hidden", type="integer", example=2),
|
||||
* @OA\Property(property="with_users", type="integer", example=4)
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="설명 변경")
|
||||
* )
|
||||
*/
|
||||
class RoleApi
|
||||
@@ -80,14 +64,13 @@ class RoleApi
|
||||
* @OA\Get(
|
||||
* path="/api/v1/roles",
|
||||
* summary="역할 목록 조회",
|
||||
* description="테넌트 범위 내 역할 목록을 페이징으로 반환합니다. (q로 이름/설명 검색, is_hidden으로 필터)",
|
||||
* description="테넌트 범위 내 역할 목록을 페이징으로 반환합니다. (q로 이름/설명 검색)",
|
||||
* tags={"Role"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)),
|
||||
* @OA\Parameter(name="size", in="query", required=false, @OA\Schema(type="integer", example=10)),
|
||||
* @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string", example="read")),
|
||||
* @OA\Parameter(name="is_hidden", in="query", required=false, description="숨김 상태 필터", @OA\Schema(type="boolean", example=false)),
|
||||
*
|
||||
* @OA\Response(response=200, description="목록 조회 성공",
|
||||
*
|
||||
@@ -225,62 +208,4 @@ public function update() {}
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/roles/stats",
|
||||
* summary="역할 통계 조회",
|
||||
* description="테넌트 범위 내 역할 통계(전체/공개/숨김/사용자 보유)를 반환합니다.",
|
||||
* tags={"Role"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(response=200, description="통계 조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/RoleStats"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function stats() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/roles/active",
|
||||
* summary="활성 역할 목록 (드롭다운용)",
|
||||
* description="숨겨지지 않은 활성 역할 목록을 이름순으로 반환합니다. (id, name, description만 포함)",
|
||||
* tags={"Role"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(response=200, description="목록 조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="admin"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="관리자")
|
||||
* )
|
||||
* ))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function active() {}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="RolePermission",
|
||||
* description="역할-퍼미션 매핑(조회/부여/회수/동기화/매트릭스/토글)"
|
||||
* description="역할-퍼미션 매핑(조회/부여/회수/동기화)"
|
||||
* )
|
||||
*/
|
||||
|
||||
@@ -96,64 +96,6 @@
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PermissionMenuTree",
|
||||
* type="object",
|
||||
* description="권한 매트릭스용 메뉴 트리",
|
||||
*
|
||||
* @OA\Property(property="menus", type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="parent_id", type="integer", nullable=true, example=null),
|
||||
* @OA\Property(property="name", type="string", example="대시보드"),
|
||||
* @OA\Property(property="url", type="string", nullable=true, example="/dashboard"),
|
||||
* @OA\Property(property="icon", type="string", nullable=true, example="dashboard"),
|
||||
* @OA\Property(property="sort_order", type="integer", example=1),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="depth", type="integer", example=0),
|
||||
* @OA\Property(property="has_children", type="boolean", example=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="permission_types", type="array", @OA\Items(type="string"), example={"view","create","update","delete","approve","export","manage"})
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="RolePermissionMatrix",
|
||||
* type="object",
|
||||
* description="역할의 권한 매트릭스",
|
||||
*
|
||||
* @OA\Property(property="role", type="object",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="admin"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="관리자")
|
||||
* ),
|
||||
* @OA\Property(property="permission_types", type="array", @OA\Items(type="string"), example={"view","create","update","delete","approve","export","manage"}),
|
||||
* @OA\Property(property="permissions", type="object", description="메뉴ID를 키로 한 권한 맵",
|
||||
* example={"101": {"view": true, "create": true}, "102": {"view": true}},
|
||||
* additionalProperties=true
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="RolePermissionToggleRequest",
|
||||
* type="object",
|
||||
* required={"menu_id","permission_type"},
|
||||
*
|
||||
* @OA\Property(property="menu_id", type="integer", example=101, description="메뉴 ID"),
|
||||
* @OA\Property(property="permission_type", type="string", example="view", description="권한 유형 (view, create, update, delete, approve, export, manage)")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="RolePermissionToggleResponse",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="menu_id", type="integer", example=101),
|
||||
* @OA\Property(property="permission_type", type="string", example="view"),
|
||||
* @OA\Property(property="granted", type="boolean", example=true, description="토글 후 권한 부여 상태")
|
||||
* )
|
||||
*/
|
||||
class RolePermissionApi
|
||||
{
|
||||
@@ -251,142 +193,4 @@ public function revoke() {}
|
||||
* )
|
||||
*/
|
||||
public function sync() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/role-permissions/menus",
|
||||
* summary="권한 매트릭스용 메뉴 트리 조회",
|
||||
* description="활성 메뉴를 플랫 배열(depth 포함)로 반환하고, 사용 가능한 권한 유형 목록을 함께 반환합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PermissionMenuTree"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function menus() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/roles/{id}/permissions/matrix",
|
||||
* summary="역할의 권한 매트릭스 조회",
|
||||
* description="해당 역할에 부여된 메뉴별 권한 매트릭스를 반환합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/RolePermissionMatrix"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function matrix() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/roles/{id}/permissions/toggle",
|
||||
* summary="특정 메뉴의 특정 권한 토글",
|
||||
* description="지정한 메뉴+권한 유형의 부여 상태를 반전합니다. 하위 메뉴에 재귀적으로 전파합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RolePermissionToggleRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="토글 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/RolePermissionToggleResponse"))
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function toggle() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/roles/{id}/permissions/allow-all",
|
||||
* summary="모든 권한 허용",
|
||||
* description="해당 역할에 모든 활성 메뉴의 모든 권한 유형을 일괄 부여합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1),
|
||||
*
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function allowAll() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/roles/{id}/permissions/deny-all",
|
||||
* summary="모든 권한 거부",
|
||||
* description="해당 역할의 모든 메뉴 권한을 일괄 제거합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1),
|
||||
*
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function denyAll() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/roles/{id}/permissions/reset",
|
||||
* summary="기본 권한으로 초기화 (view만 허용)",
|
||||
* description="해당 역할의 모든 권한을 제거한 후, 모든 활성 메뉴에 view 권한만 부여합니다.",
|
||||
* tags={"RolePermission"},
|
||||
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1),
|
||||
*
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function reset() {}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Google Cloud Storage 설정
|
||||
*
|
||||
* 우선순위:
|
||||
* 1. DB 설정 (ai_configs 테이블) - UI에서 오버라이드 가능
|
||||
* 2. 환경변수 (.env)
|
||||
* 3. 레거시 파일 (/sales/apikey/)
|
||||
*/
|
||||
|
||||
return [
|
||||
// 버킷 이름
|
||||
'bucket_name' => env('GCS_BUCKET_NAME'),
|
||||
|
||||
// 서비스 계정 파일 경로
|
||||
'service_account_path' => env('GCS_SERVICE_ACCOUNT_PATH', '/var/www/mng/apikey/google_service_account.json'),
|
||||
|
||||
// DB 설정 사용 여부 (false면 .env만 사용)
|
||||
'use_db_config' => env('GCS_USE_DB_CONFIG', true),
|
||||
];
|
||||
@@ -24,7 +24,7 @@
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => App\Models\Permissions\Role::class,
|
||||
'role' => Spatie\Permission\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -58,48 +58,4 @@
|
||||
'exchange_secret' => env('INTERNAL_EXCHANGE_SECRET'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Claude AI
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'claude' => [
|
||||
'api_key' => env('CLAUDE_API_KEY'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Google Cloud (STT + GCS Storage)
|
||||
|--------------------------------------------------------------------------
|
||||
| Google Cloud 서비스 인증 및 음성인식(STT) 설정
|
||||
*/
|
||||
'google' => [
|
||||
'credentials_path' => env('GOOGLE_APPLICATION_CREDENTIALS'),
|
||||
'storage_bucket' => env('GOOGLE_STORAGE_BUCKET'),
|
||||
'location' => env('GOOGLE_STT_LOCATION', 'asia-southeast1'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Vertex AI (Veo 영상 생성 등)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'vertex_ai' => [
|
||||
'project_id' => env('VERTEX_AI_PROJECT_ID', 'codebridge-chatbot'),
|
||||
'location' => env('VERTEX_AI_LOCATION', 'us-central1'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Barobill API (SOAP)
|
||||
|--------------------------------------------------------------------------
|
||||
| 바로빌 SOAP 연동 설정 (계좌/카드/인증서 등)
|
||||
*/
|
||||
'barobill' => [
|
||||
'cert_key_test' => env('BAROBILL_CERT_KEY_TEST', ''),
|
||||
'cert_key_prod' => env('BAROBILL_CERT_KEY_PROD', ''),
|
||||
'corp_num' => env('BAROBILL_CORP_NUM', ''),
|
||||
'test_mode' => env('BAROBILL_TEST_MODE', true),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?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::table('cards', function (Blueprint $table) {
|
||||
$table->string('card_type', 50)->nullable()->after('card_company')->comment('카드 종류 (corporate_1, personal 등)');
|
||||
$table->string('alias', 100)->nullable()->after('card_name')->comment('카드 별칭');
|
||||
$table->text('cvc_encrypted')->nullable()->after('card_password_encrypted')->comment('암호화된 CVC/CVV');
|
||||
$table->unsignedTinyInteger('payment_day')->nullable()->after('cvc_encrypted')->comment('결제일 (1-31)');
|
||||
$table->decimal('total_limit', 15, 2)->nullable()->after('payment_day')->comment('총 한도');
|
||||
$table->decimal('used_amount', 15, 2)->nullable()->after('total_limit')->comment('사용 금액');
|
||||
$table->decimal('remaining_limit', 15, 2)->nullable()->after('used_amount')->comment('잔여 한도');
|
||||
$table->boolean('is_manual')->default(false)->after('status')->comment('수기 등록 여부');
|
||||
$table->text('memo')->nullable()->after('assigned_user_id')->comment('메모');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('cards', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'card_type',
|
||||
'alias',
|
||||
'cvc_encrypted',
|
||||
'payment_day',
|
||||
'total_limit',
|
||||
'used_amount',
|
||||
'remaining_limit',
|
||||
'is_manual',
|
||||
'memo',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -32,16 +32,6 @@
|
||||
// Server errors
|
||||
'server_error' => 'An internal server error occurred.', // 5xx
|
||||
|
||||
// Role/Permission related
|
||||
'role' => [
|
||||
'not_found' => 'Role not found.',
|
||||
'permission_input_required' => 'Either permission_names or menus+actions is required.',
|
||||
'no_valid_permissions' => 'No valid permissions found.',
|
||||
'role_input_required' => 'Either role_names or role_ids is required.',
|
||||
'no_valid_roles' => 'No valid roles found.',
|
||||
'user_not_found' => 'User not found.',
|
||||
],
|
||||
|
||||
// Estimate related errors
|
||||
'estimate' => [
|
||||
'cannot_delete_sent_or_approved' => 'Cannot delete estimates that have been sent or approved.',
|
||||
|
||||
@@ -117,11 +117,4 @@
|
||||
'cancelled' => 'Tax invoice has been cancelled.',
|
||||
'bulk_issued' => 'Tax invoices have been issued in bulk.',
|
||||
],
|
||||
|
||||
// Barobill Integration
|
||||
'barobill' => [
|
||||
'login_success' => 'Barobill login registered successfully.',
|
||||
'signup_success' => 'Barobill signup completed successfully.',
|
||||
'connection_success' => 'Barobill connection test succeeded.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -39,16 +39,6 @@
|
||||
// 서버 오류
|
||||
'server_error' => '서버 처리 중 오류가 발생했습니다.', // 5xx 일반
|
||||
|
||||
// 역할/권한 관련
|
||||
'role' => [
|
||||
'not_found' => '역할을 찾을 수 없습니다.',
|
||||
'permission_input_required' => 'permission_names 또는 menus+actions 중 하나는 필요합니다.',
|
||||
'no_valid_permissions' => '유효한 퍼미션이 없습니다.',
|
||||
'role_input_required' => 'role_names 또는 role_ids 중 하나는 필요합니다.',
|
||||
'no_valid_roles' => '유효한 역할이 없습니다.',
|
||||
'user_not_found' => '사용자를 찾을 수 없습니다.',
|
||||
],
|
||||
|
||||
// 견적 관련 에러
|
||||
'estimate' => [
|
||||
'cannot_delete_sent_or_approved' => '발송되었거나 승인된 견적은 삭제할 수 없습니다.',
|
||||
@@ -291,11 +281,6 @@
|
||||
'no_base_salary' => '기본급이 설정되지 않았습니다.',
|
||||
],
|
||||
|
||||
// 바로빌 연동 관련
|
||||
'barobill' => [
|
||||
'member_not_found' => '바로빌 회원 정보가 등록되어 있지 않습니다.',
|
||||
],
|
||||
|
||||
// 세금계산서 관련
|
||||
'tax_invoice' => [
|
||||
'not_found' => '세금계산서 정보를 찾을 수 없습니다.',
|
||||
|
||||
@@ -566,13 +566,6 @@
|
||||
'downloaded' => '문서가 다운로드되었습니다.',
|
||||
],
|
||||
|
||||
// 바로빌 연동
|
||||
'barobill' => [
|
||||
'login_success' => '바로빌 로그인 정보가 등록되었습니다.',
|
||||
'signup_success' => '바로빌 회원가입이 완료되었습니다.',
|
||||
'connection_success' => '바로빌 연동 테스트 성공',
|
||||
],
|
||||
|
||||
// CEO 대시보드 부가세 현황
|
||||
'vat' => [
|
||||
'sales_tax' => '매출세액',
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
});
|
||||
|
||||
// Role API
|
||||
Route::middleware(['perm.map', 'permission'])->prefix('roles')->group(function () {
|
||||
Route::prefix('roles')->group(function () {
|
||||
Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index');
|
||||
Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store');
|
||||
Route::get('/stats', [RoleController::class, 'stats'])->name('v1.roles.stats');
|
||||
@@ -68,10 +68,10 @@
|
||||
});
|
||||
|
||||
// Role Permission API - 공통
|
||||
Route::middleware(['perm.map', 'permission'])->get('/role-permissions/menus', [RolePermissionController::class, 'menus'])->name('v1.roles.perms.menus');
|
||||
Route::get('/role-permissions/menus', [RolePermissionController::class, 'menus'])->name('v1.roles.perms.menus');
|
||||
|
||||
// Role Permission API - 역할별
|
||||
Route::middleware(['perm.map', 'permission'])->prefix('roles/{id}/permissions')->group(function () {
|
||||
Route::prefix('roles/{id}/permissions')->group(function () {
|
||||
Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index');
|
||||
Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant');
|
||||
Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke');
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
use App\Http\Controllers\Api\V1\BadDebtController;
|
||||
use App\Http\Controllers\Api\V1\BankAccountController;
|
||||
use App\Http\Controllers\Api\V1\BankTransactionController;
|
||||
use App\Http\Controllers\Api\V1\BarobillController;
|
||||
use App\Http\Controllers\Api\V1\BarobillSettingController;
|
||||
use App\Http\Controllers\Api\V1\BillController;
|
||||
use App\Http\Controllers\Api\V1\CalendarController;
|
||||
use App\Http\Controllers\Api\V1\CardController;
|
||||
use App\Http\Controllers\Api\V1\CardTransactionController;
|
||||
use App\Http\Controllers\Api\V1\ComprehensiveAnalysisController;
|
||||
use App\Http\Controllers\Api\V1\CorporateCardController;
|
||||
use App\Http\Controllers\Api\V1\DailyReportController;
|
||||
use App\Http\Controllers\Api\V1\DepositController;
|
||||
use App\Http\Controllers\Api\V1\EntertainmentController;
|
||||
@@ -66,17 +64,6 @@
|
||||
Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary');
|
||||
});
|
||||
|
||||
// CorporateCard API (법인카드 관리)
|
||||
Route::prefix('corporate-cards')->group(function () {
|
||||
Route::get('', [CorporateCardController::class, 'index'])->name('v1.corporate-cards.index');
|
||||
Route::post('', [CorporateCardController::class, 'store'])->name('v1.corporate-cards.store');
|
||||
Route::get('/active', [CorporateCardController::class, 'active'])->name('v1.corporate-cards.active');
|
||||
Route::get('/{id}', [CorporateCardController::class, 'show'])->whereNumber('id')->name('v1.corporate-cards.show');
|
||||
Route::put('/{id}', [CorporateCardController::class, 'update'])->whereNumber('id')->name('v1.corporate-cards.update');
|
||||
Route::delete('/{id}', [CorporateCardController::class, 'destroy'])->whereNumber('id')->name('v1.corporate-cards.destroy');
|
||||
Route::patch('/{id}/toggle', [CorporateCardController::class, 'toggle'])->whereNumber('id')->name('v1.corporate-cards.toggle');
|
||||
});
|
||||
|
||||
// Deposit API (입금 관리)
|
||||
Route::prefix('deposits')->group(function () {
|
||||
Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index');
|
||||
@@ -267,32 +254,18 @@
|
||||
Route::post('/test-connection', [BarobillSettingController::class, 'testConnection'])->name('v1.barobill-settings.test-connection');
|
||||
});
|
||||
|
||||
// Barobill Integration API (바로빌 연동 관리)
|
||||
Route::prefix('barobill')->group(function () {
|
||||
Route::post('/login', [BarobillController::class, 'login'])->name('v1.barobill.login');
|
||||
Route::post('/signup', [BarobillController::class, 'signup'])->name('v1.barobill.signup');
|
||||
Route::get('/bank-service-url', [BarobillController::class, 'bankServiceUrl'])->name('v1.barobill.bank-service-url');
|
||||
Route::get('/status', [BarobillController::class, 'status'])->name('v1.barobill.status');
|
||||
Route::get('/account-link-url', [BarobillController::class, 'accountLinkUrl'])->name('v1.barobill.account-link-url');
|
||||
Route::get('/card-link-url', [BarobillController::class, 'cardLinkUrl'])->name('v1.barobill.card-link-url');
|
||||
Route::get('/certificate-url', [BarobillController::class, 'certificateUrl'])->name('v1.barobill.certificate-url');
|
||||
});
|
||||
|
||||
// Tax Invoice API (세금계산서)
|
||||
Route::prefix('tax-invoices')->group(function () {
|
||||
Route::get('', [TaxInvoiceController::class, 'index'])->name('v1.tax-invoices.index');
|
||||
Route::post('', [TaxInvoiceController::class, 'store'])->name('v1.tax-invoices.store');
|
||||
Route::get('/summary', [TaxInvoiceController::class, 'summary'])->name('v1.tax-invoices.summary');
|
||||
Route::get('/supplier-settings', [TaxInvoiceController::class, 'supplierSettings'])->name('v1.tax-invoices.supplier-settings');
|
||||
Route::put('/supplier-settings', [TaxInvoiceController::class, 'saveSupplierSettings'])->name('v1.tax-invoices.save-supplier-settings');
|
||||
Route::post('/issue-direct', [TaxInvoiceController::class, 'storeAndIssue'])->name('v1.tax-invoices.issue-direct');
|
||||
Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue');
|
||||
Route::get('/{id}', [TaxInvoiceController::class, 'show'])->whereNumber('id')->name('v1.tax-invoices.show');
|
||||
Route::put('/{id}', [TaxInvoiceController::class, 'update'])->whereNumber('id')->name('v1.tax-invoices.update');
|
||||
Route::delete('/{id}', [TaxInvoiceController::class, 'destroy'])->whereNumber('id')->name('v1.tax-invoices.destroy');
|
||||
Route::post('/{id}/issue', [TaxInvoiceController::class, 'issue'])->whereNumber('id')->name('v1.tax-invoices.issue');
|
||||
Route::post('/{id}/cancel', [TaxInvoiceController::class, 'cancel'])->whereNumber('id')->name('v1.tax-invoices.cancel');
|
||||
Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status');
|
||||
Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue');
|
||||
});
|
||||
|
||||
// Bad Debt API (악성채권 추심관리)
|
||||
|
||||
Reference in New Issue
Block a user