diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4f74ed7 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 7afef19..31809a0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ !storage/.gitignore .env .env.* +!.env.example .phpunit.result.cache Homestead.yaml Homestead.json diff --git a/app/Http/Controllers/Api/V1/BarobillController.php b/app/Http/Controllers/Api/V1/BarobillController.php deleted file mode 100644 index c8b58c4..0000000 --- a/app/Http/Controllers/Api/V1/BarobillController.php +++ /dev/null @@ -1,73 +0,0 @@ - $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') - ); - } -} diff --git a/app/Http/Controllers/Api/V1/CorporateCardController.php b/app/Http/Controllers/Api/V1/CorporateCardController.php deleted file mode 100644 index d7e035c..0000000 --- a/app/Http/Controllers/Api/V1/CorporateCardController.php +++ /dev/null @@ -1,97 +0,0 @@ -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')); - } -} diff --git a/app/Http/Controllers/Api/V1/RoleController.php b/app/Http/Controllers/Api/V1/RoleController.php index 7f5b056..789a159 100644 --- a/app/Http/Controllers/Api/V1/RoleController.php +++ b/app/Http/Controllers/Api/V1/RoleController.php @@ -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')); } diff --git a/app/Http/Controllers/Api/V1/RolePermissionController.php b/app/Http/Controllers/Api/V1/RolePermissionController.php index ee20ce0..1e5c2c3 100644 --- a/app/Http/Controllers/Api/V1/RolePermissionController.php +++ b/app/Http/Controllers/Api/V1/RolePermissionController.php @@ -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')); } diff --git a/app/Http/Controllers/Api/V1/TaxInvoiceController.php b/app/Http/Controllers/Api/V1/TaxInvoiceController.php index e0f9dc0..2fe8dcf 100644 --- a/app/Http/Controllers/Api/V1/TaxInvoiceController.php +++ b/app/Http/Controllers/Api/V1/TaxInvoiceController.php @@ -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') ); } } diff --git a/app/Http/Controllers/Api/V1/UserRoleController.php b/app/Http/Controllers/Api/V1/UserRoleController.php index 926ef1b..afe4cb7 100644 --- a/app/Http/Controllers/Api/V1/UserRoleController.php +++ b/app/Http/Controllers/Api/V1/UserRoleController.php @@ -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()); + }, '사용자의 역할 동기화'); } } diff --git a/app/Http/Requests/Authz/RoleIndexRequest.php b/app/Http/Requests/Authz/RoleIndexRequest.php deleted file mode 100644 index 523b4a7..0000000 --- a/app/Http/Requests/Authz/RoleIndexRequest.php +++ /dev/null @@ -1,23 +0,0 @@ - 'sometimes|integer|min:1', - 'size' => 'sometimes|integer|min:1|max:100', - 'q' => 'sometimes|nullable|string|max:100', - 'is_hidden' => 'sometimes|boolean', - ]; - } -} diff --git a/app/Http/Requests/Authz/RolePermissionGrantRequest.php b/app/Http/Requests/Authz/RolePermissionGrantRequest.php deleted file mode 100644 index c3ed1b6..0000000 --- a/app/Http/Requests/Authz/RolePermissionGrantRequest.php +++ /dev/null @@ -1,38 +0,0 @@ - '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')); - } - }); - } -} diff --git a/app/Http/Requests/Authz/RolePermissionToggleRequest.php b/app/Http/Requests/Authz/RolePermissionToggleRequest.php deleted file mode 100644 index 668b81b..0000000 --- a/app/Http/Requests/Authz/RolePermissionToggleRequest.php +++ /dev/null @@ -1,24 +0,0 @@ - 'required|integer|min:1', - 'permission_type' => ['required', 'string', Rule::in($permissionTypes)], - ]; - } -} diff --git a/app/Http/Requests/Authz/RoleStoreRequest.php b/app/Http/Requests/Authz/RoleStoreRequest.php deleted file mode 100644 index 772b32b..0000000 --- a/app/Http/Requests/Authz/RoleStoreRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - [ - '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', - ]; - } -} diff --git a/app/Http/Requests/Authz/RoleUpdateRequest.php b/app/Http/Requests/Authz/RoleUpdateRequest.php deleted file mode 100644 index 1698caf..0000000 --- a/app/Http/Requests/Authz/RoleUpdateRequest.php +++ /dev/null @@ -1,32 +0,0 @@ -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', - ]; - } -} diff --git a/app/Http/Requests/Authz/UserRoleGrantRequest.php b/app/Http/Requests/Authz/UserRoleGrantRequest.php deleted file mode 100644 index 491ab55..0000000 --- a/app/Http/Requests/Authz/UserRoleGrantRequest.php +++ /dev/null @@ -1,33 +0,0 @@ - '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')); - } - }); - } -} diff --git a/app/Http/Requests/Barobill/BankServiceUrlRequest.php b/app/Http/Requests/Barobill/BankServiceUrlRequest.php deleted file mode 100644 index ece0f68..0000000 --- a/app/Http/Requests/Barobill/BankServiceUrlRequest.php +++ /dev/null @@ -1,21 +0,0 @@ - ['required', 'string', 'max:10'], - 'account_type' => ['required', 'string', 'max:10'], - ]; - } -} diff --git a/app/Http/Requests/Barobill/BarobillLoginRequest.php b/app/Http/Requests/Barobill/BarobillLoginRequest.php deleted file mode 100644 index 7bb41f4..0000000 --- a/app/Http/Requests/Barobill/BarobillLoginRequest.php +++ /dev/null @@ -1,21 +0,0 @@ - ['required', 'string', 'max:50'], - 'password' => ['required', 'string', 'max:255'], - ]; - } -} diff --git a/app/Http/Requests/Barobill/BarobillSignupRequest.php b/app/Http/Requests/Barobill/BarobillSignupRequest.php deleted file mode 100644 index 0047ad0..0000000 --- a/app/Http/Requests/Barobill/BarobillSignupRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - ['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'], - ]; - } -} diff --git a/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php b/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php deleted file mode 100644 index b1a28bd..0000000 --- a/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php +++ /dev/null @@ -1,28 +0,0 @@ - ['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'], - ]; - } -} diff --git a/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php b/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php index d830591..cea8b9a 100644 --- a/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php +++ b/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php b/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php index 0d44a72..45afdb6 100644 --- a/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php +++ b/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/V1/Card/StoreCardRequest.php b/app/Http/Requests/V1/Card/StoreCardRequest.php index 72051ab..20123e9 100644 --- a/app/Http/Requests/V1/Card/StoreCardRequest.php +++ b/app/Http/Requests/V1/Card/StoreCardRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/V1/Card/UpdateCardRequest.php b/app/Http/Requests/V1/Card/UpdateCardRequest.php index d9cb68d..2506ab6 100644 --- a/app/Http/Requests/V1/Card/UpdateCardRequest.php +++ b/app/Http/Requests/V1/Card/UpdateCardRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/V1/CorporateCard/StoreCorporateCardRequest.php b/app/Http/Requests/V1/CorporateCard/StoreCorporateCardRequest.php deleted file mode 100644 index d8f4293..0000000 --- a/app/Http/Requests/V1/CorporateCard/StoreCorporateCardRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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'], - ]; - } -} diff --git a/app/Http/Requests/V1/CorporateCard/UpdateCorporateCardRequest.php b/app/Http/Requests/V1/CorporateCard/UpdateCorporateCardRequest.php deleted file mode 100644 index 4b6c25b..0000000 --- a/app/Http/Requests/V1/CorporateCard/UpdateCorporateCardRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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'], - ]; - } -} diff --git a/app/Models/Permissions/Role.php b/app/Models/Permissions/Role.php index fc85a49..cb529e4 100644 --- a/app/Models/Permissions/Role.php +++ b/app/Models/Permissions/Role.php @@ -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' + ); + } + /** * 스코프: 공개된 역할만 */ diff --git a/app/Models/Tenants/BankAccount.php b/app/Models/Tenants/BankAccount.php index 2e7860b..416663d 100644 --- a/app/Models/Tenants/BankAccount.php +++ b/app/Models/Tenants/BankAccount.php @@ -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; } } diff --git a/app/Models/Tenants/BarobillConfig.php b/app/Models/Tenants/BarobillConfig.php deleted file mode 100644 index faae2c6..0000000 --- a/app/Models/Tenants/BarobillConfig.php +++ /dev/null @@ -1,40 +0,0 @@ - '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(); - } -} diff --git a/app/Models/Tenants/BarobillMember.php b/app/Models/Tenants/BarobillMember.php deleted file mode 100644 index 0aed74c..0000000 --- a/app/Models/Tenants/BarobillMember.php +++ /dev/null @@ -1,66 +0,0 @@ - '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'; - } -} diff --git a/app/Models/Tenants/Card.php b/app/Models/Tenants/Card.php index a8a4cc8..eb1eaf7 100644 --- a/app/Models/Tenants/Card.php +++ b/app/Models/Tenants/Card.php @@ -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(); - } - // ========================================================================= // 헬퍼 메서드 // ========================================================================= diff --git a/app/Models/Tenants/CorporateCard.php b/app/Models/Tenants/CorporateCard.php deleted file mode 100644 index aafeddb..0000000 --- a/app/Models/Tenants/CorporateCard.php +++ /dev/null @@ -1,110 +0,0 @@ - '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); - } -} diff --git a/app/Services/Authz/RolePermissionService.php b/app/Services/Authz/RolePermissionService.php index d6452b3..86e14d7 100644 --- a/app/Services/Authz/RolePermissionService.php +++ b/app/Services/Authz/RolePermissionService.php @@ -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'; } } diff --git a/app/Services/Authz/RoleService.php b/app/Services/Authz/RoleService.php index be1c812..5cce42b 100644 --- a/app/Services/Authz/RoleService.php +++ b/app/Services/Authz/RoleService.php @@ -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'; } diff --git a/app/Services/Authz/UserRoleService.php b/app/Services/Authz/UserRoleService.php index 7d075a5..2098d4c 100644 --- a/app/Services/Authz/UserRoleService.php +++ b/app/Services/Authz/UserRoleService.php @@ -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'; } } diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index 1cdf3d2..55669e8 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -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, ]); diff --git a/app/Services/Barobill/BarobillSoapService.php b/app/Services/Barobill/BarobillSoapService.php deleted file mode 100644 index 5113b1a..0000000 --- a/app/Services/Barobill/BarobillSoapService.php +++ /dev/null @@ -1,604 +0,0 @@ - '사업자번호가 설정되지 않았거나 유효하지 않습니다.', - -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']]; - } -} diff --git a/app/Services/BarobillService.php b/app/Services/BarobillService.php index b9517bc..1d8451f 100644 --- a/app/Services/BarobillService.php +++ b/app/Services/BarobillService.php @@ -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,31 +89,26 @@ 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 (! empty($response['AccessToken'])) { + // 검증 성공 시 verified_at 업데이트 + $setting->verified_at = now(); + $setting->save(); - // 양수 또는 문자열 토큰 = 성공 - if (is_string($resultData) || (is_numeric($resultData) && $resultData > 0)) { - $setting->verified_at = now(); - $setting->save(); - - return [ - 'success' => true, - 'message' => __('message.barobill.connection_success'), - 'verified_at' => $setting->verified_at->toDateTimeString(), - ]; - } + return [ + 'success' => true, + 'message' => __('message.barobill.connection_success'), + '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'), - ]; - } + return [ + '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'), + ]; } - // API 실패 시 형식 검증 결과만 반환 + // 응답 형식이 다른 경우 (결과 코드 방식) + 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')), + ]; + } + + // 기본 응답 (체크섬만 통과한 경우) 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']; + if (! empty($response['State'])) { + $taxInvoice->nts_send_status = $response['State']; - // SOAP 결과 객체에서 상태 추출 - $state = is_object($stateData) ? ($stateData->NTSSendState ?? null) : null; - - if ($state !== null) { - $taxInvoice->nts_send_status = (string) $state; - - // 국세청 전송 완료 시 상태 업데이트 - if (in_array($state, [3, '3', '전송완료']) && ! $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->save(); + // 국세청 전송 완료 시 상태 업데이트 + if ($response['State'] === '전송완료' && ! $taxInvoice->sent_at) { + $taxInvoice->status = TaxInvoice::STATUS_SENT; + $taxInvoice->sent_at = now(); + $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 => '정발행', }; } } diff --git a/app/Services/CardService.php b/app/Services/CardService.php index f518f5b..4558c8f 100644 --- a/app/Services/CardService.php +++ b/app/Services/CardService.php @@ -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(); }); } diff --git a/app/Services/CorporateCardService.php b/app/Services/CorporateCardService.php deleted file mode 100644 index 43068f3..0000000 --- a/app/Services/CorporateCardService.php +++ /dev/null @@ -1,171 +0,0 @@ -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(); - } -} diff --git a/app/Services/TaxInvoiceService.php b/app/Services/TaxInvoiceService.php index 796cba6..19cd6c8 100644 --- a/app/Services/TaxInvoiceService.php +++ b/app/Services/TaxInvoiceService.php @@ -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); - }); - } - // ========================================================================= // 통계 // ========================================================================= diff --git a/app/Swagger/v1/RoleApi.php b/app/Swagger/v1/RoleApi.php index 272e153..8c7700d 100644 --- a/app/Swagger/v1/RoleApi.php +++ b/app/Swagger/v1/RoleApi.php @@ -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() {} } diff --git a/app/Swagger/v1/RolePermissionApi.php b/app/Swagger/v1/RolePermissionApi.php index 00eedd0..8f83b01 100644 --- a/app/Swagger/v1/RolePermissionApi.php +++ b/app/Swagger/v1/RolePermissionApi.php @@ -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() {} } diff --git a/config/gcs.php b/config/gcs.php deleted file mode 100644 index 2305a9f..0000000 --- a/config/gcs.php +++ /dev/null @@ -1,21 +0,0 @@ - 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), -]; diff --git a/config/permission.php b/config/permission.php index d2f1004..e989f4d 100644 --- a/config/permission.php +++ b/config/permission.php @@ -24,7 +24,7 @@ * `Spatie\Permission\Contracts\Role` contract. */ - 'role' => App\Models\Permissions\Role::class, + 'role' => Spatie\Permission\Models\Role::class, ], diff --git a/config/services.php b/config/services.php index 4631a9d..3865719 100644 --- a/config/services.php +++ b/config/services.php @@ -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), - ], - ]; diff --git a/database/migrations/2026_02_21_100000_add_detail_fields_to_cards_table.php b/database/migrations/2026_02_21_100000_add_detail_fields_to_cards_table.php deleted file mode 100644 index 5902baa..0000000 --- a/database/migrations/2026_02_21_100000_add_detail_fields_to_cards_table.php +++ /dev/null @@ -1,46 +0,0 @@ -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', - ]); - }); - } -}; diff --git a/lang/en/error.php b/lang/en/error.php index 9139d20..576ec04 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -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.', diff --git a/lang/en/message.php b/lang/en/message.php index 40d7ebf..f2036b5 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -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.', - ], ]; diff --git a/lang/ko/error.php b/lang/ko/error.php index 59f5b04..f029c2b 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -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' => '세금계산서 정보를 찾을 수 없습니다.', diff --git a/lang/ko/message.php b/lang/ko/message.php index a907f89..ec881e2 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -566,13 +566,6 @@ 'downloaded' => '문서가 다운로드되었습니다.', ], - // 바로빌 연동 - 'barobill' => [ - 'login_success' => '바로빌 로그인 정보가 등록되었습니다.', - 'signup_success' => '바로빌 회원가입이 완료되었습니다.', - 'connection_success' => '바로빌 연동 테스트 성공', - ], - // CEO 대시보드 부가세 현황 'vat' => [ 'sales_tax' => '매출세액', diff --git a/routes/api/v1/common.php b/routes/api/v1/common.php index 8dca56e..ede81c7 100644 --- a/routes/api/v1/common.php +++ b/routes/api/v1/common.php @@ -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'); diff --git a/routes/api/v1/finance.php b/routes/api/v1/finance.php index 5668842..2d03aca 100644 --- a/routes/api/v1/finance.php +++ b/routes/api/v1/finance.php @@ -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 (악성채권 추심관리)