diff --git a/app/Http/Controllers/Admin/Product/AdminSaleCodeController.php b/app/Http/Controllers/Admin/Product/AdminSaleCodeController.php index a4eb8bb..3051ee7 100644 --- a/app/Http/Controllers/Admin/Product/AdminSaleCodeController.php +++ b/app/Http/Controllers/Admin/Product/AdminSaleCodeController.php @@ -21,32 +21,75 @@ final class AdminSaleCodeController public function storeProvider(Request $request) { $data = $request->validate([ - 'code' => ['required', 'string', 'max:30'], - 'name' => ['required', 'string', 'max:50'], - 'is_active' => ['required', 'in:0,1'], + 'code' => ['required', 'string', 'max:30'], + 'name' => ['required', 'string', 'max:50'], + 'transport_type' => ['required', 'in:HTTP_FORM,HTTP_ENCRYPTED,TCP_SOCKET'], + 'base_url' => ['nullable', 'string', 'max:255'], + 'host' => ['nullable', 'string', 'max:100'], + 'port' => ['nullable', 'integer', 'min:1', 'max:65535'], + 'timeout_connect_sec' => ['required', 'integer', 'min:1', 'max:120'], + 'timeout_read_sec' => ['required', 'integer', 'min:1', 'max:300'], + 'charset' => ['required', 'string', 'max:20'], + 'response_format_default' => ['nullable', 'string', 'max:10'], + 'is_test_mode' => ['required', 'in:0,1'], + 'supports_issue' => ['required', 'in:0,1'], + 'supports_cancel' => ['required', 'in:0,1'], + 'supports_resend' => ['required', 'in:0,1'], + 'supports_cancel_check' => ['required', 'in:0,1'], + 'supports_network_cancel' => ['required', 'in:0,1'], + 'config_json' => ['nullable', 'string'], + 'is_active' => ['required', 'in:0,1'], ]); $res = $this->service->storeProvider($data, (int) auth('admin')->id(), $request->ip(), $request->userAgent() ?? ''); - return redirect()->back()->with('toast', ['type' => $res['ok'] ? 'success' : 'danger', 'title' => '알림', 'message' => $res['message']]); + return redirect()->back()->with('toast', [ + 'type' => $res['ok'] ? 'success' : 'danger', + 'title' => '알림', + 'message' => $res['message'], + ]); } public function updateProvider(int $id, Request $request) { $data = $request->validate([ - 'name' => ['required', 'string', 'max:50'], - 'is_active' => ['required', 'in:0,1'], + 'name' => ['required', 'string', 'max:50'], + 'transport_type' => ['required', 'in:HTTP_FORM,HTTP_ENCRYPTED,TCP_SOCKET'], + 'base_url' => ['nullable', 'string', 'max:255'], + 'host' => ['nullable', 'string', 'max:100'], + 'port' => ['nullable', 'integer', 'min:1', 'max:65535'], + 'timeout_connect_sec' => ['required', 'integer', 'min:1', 'max:120'], + 'timeout_read_sec' => ['required', 'integer', 'min:1', 'max:300'], + 'charset' => ['required', 'string', 'max:20'], + 'response_format_default' => ['nullable', 'string', 'max:10'], + 'is_test_mode' => ['required', 'in:0,1'], + 'supports_issue' => ['required', 'in:0,1'], + 'supports_cancel' => ['required', 'in:0,1'], + 'supports_resend' => ['required', 'in:0,1'], + 'supports_cancel_check' => ['required', 'in:0,1'], + 'supports_network_cancel' => ['required', 'in:0,1'], + 'config_json' => ['nullable', 'string'], + 'is_active' => ['required', 'in:0,1'], ]); $res = $this->service->updateProvider($id, $data, (int) auth('admin')->id(), $request->ip(), $request->userAgent() ?? ''); - return redirect()->back()->with('toast', ['type' => $res['ok'] ? 'success' : 'danger', 'title' => '알림', 'message' => $res['message']]); + return redirect()->back()->with('toast', [ + 'type' => $res['ok'] ? 'success' : 'danger', + 'title' => '알림', + 'message' => $res['message'], + ]); } public function destroyProvider(int $id, Request $request) { $res = $this->service->deleteProvider($id, (int) auth('admin')->id(), $request->ip(), $request->userAgent() ?? ''); - return redirect()->back()->with('toast', ['type' => $res['ok'] ? 'success' : 'danger', 'title' => '알림', 'message' => $res['message']]); + + return redirect()->back()->with('toast', [ + 'type' => $res['ok'] ? 'success' : 'danger', + 'title' => '알림', + 'message' => $res['message'], + ]); } // --- Product Code --- diff --git a/app/Repositories/Admin/Product/AdminSaleCodeRepository.php b/app/Repositories/Admin/Product/AdminSaleCodeRepository.php index 62847c9..72dc749 100644 --- a/app/Repositories/Admin/Product/AdminSaleCodeRepository.php +++ b/app/Repositories/Admin/Product/AdminSaleCodeRepository.php @@ -6,15 +6,12 @@ use Illuminate\Support\Facades\DB; final class AdminSaleCodeRepository { - // ========================================== - // 1. Providers (API 연동사) - // ========================================== public function getAllProviders(): array { return DB::table('gc_api_providers') ->orderBy('sort_order') ->get() - ->map(fn($r) => (array)$r) + ->map(fn ($r) => (array) $r) ->all(); } @@ -27,13 +24,17 @@ final class AdminSaleCodeRepository { $data['created_at'] = now()->format('Y-m-d H:i:s'); $data['updated_at'] = $data['created_at']; + return DB::table('gc_api_providers')->insertGetId($data); } public function updateProvider(int $id, array $data): bool { $data['updated_at'] = now()->format('Y-m-d H:i:s'); - return DB::table('gc_api_providers')->where('id', $id)->update($data) > 0; + + return DB::table('gc_api_providers') + ->where('id', $id) + ->update($data) > 0; } public function deleteProvider(int $id): bool @@ -41,16 +42,13 @@ final class AdminSaleCodeRepository return DB::table('gc_api_providers')->where('id', $id)->delete() > 0; } - // ========================================== - // 2. Product Codes (상품 코드 매핑) - // ========================================== public function getAllCodes(): array { return DB::table('gc_api_product_codes') ->orderBy('provider_id') ->orderBy('sort_order') ->get() - ->map(fn($r) => (array)$r) + ->map(fn ($r) => (array) $r) ->all(); } @@ -63,13 +61,17 @@ final class AdminSaleCodeRepository { $data['created_at'] = now()->format('Y-m-d H:i:s'); $data['updated_at'] = $data['created_at']; + return DB::table('gc_api_product_codes')->insertGetId($data); } public function updateCode(int $id, array $data): bool { $data['updated_at'] = now()->format('Y-m-d H:i:s'); - return DB::table('gc_api_product_codes')->where('id', $id)->update($data) > 0; + + return DB::table('gc_api_product_codes') + ->where('id', $id) + ->update($data) > 0; } public function deleteCode(int $id): bool @@ -86,6 +88,8 @@ final class AdminSaleCodeRepository public function countCodesByProvider(int $providerId): int { - return DB::table('gc_api_product_codes')->where('provider_id', $providerId)->count(); + return DB::table('gc_api_product_codes') + ->where('provider_id', $providerId) + ->count(); } } diff --git a/app/Services/Admin/Product/AdminSaleCodeService.php b/app/Services/Admin/Product/AdminSaleCodeService.php index beec0a0..2b2ceab 100644 --- a/app/Services/Admin/Product/AdminSaleCodeService.php +++ b/app/Services/Admin/Product/AdminSaleCodeService.php @@ -13,9 +13,6 @@ final class AdminSaleCodeService private readonly AdminAuditService $audit, ) {} - /** - * 화면 출력용 트리 구조 생성 (연동사 -> 하위 상품코드) - */ public function getGroupedTree(): array { $providers = $this->repo->getAllProviders(); @@ -36,23 +33,19 @@ final class AdminSaleCodeService return array_values($tree); } - // --- Provider --- public function storeProvider(array $input, int $actorAdminId, string $ip, string $ua): array { try { return DB::transaction(function () use ($input, $actorAdminId, $ip, $ua) { - $data = [ - 'code' => strtoupper(trim($input['code'])), - 'name' => trim($input['name']), - 'is_active' => (int)$input['is_active'], - 'sort_order' => 0, - ]; + $data = $this->buildProviderPayload($input, true); $newId = $this->repo->insertProvider($data); $this->audit->log($actorAdminId, 'admin.sale_code.provider.create', 'api_provider', $newId, null, $data, $ip, $ua); return ['ok' => true, 'message' => '연동사가 등록되었습니다.']; }); + } catch (\InvalidArgumentException $e) { + return ['ok' => false, 'message' => $e->getMessage()]; } catch (\Throwable $e) { return ['ok' => false, 'message' => '연동사 코드가 중복되거나 저장 오류가 발생했습니다.']; } @@ -63,18 +56,29 @@ final class AdminSaleCodeService try { return DB::transaction(function () use ($id, $input, $actorAdminId, $ip, $ua) { $before = $this->repo->findProvider($id); - if (!$before) return ['ok' => false, 'message' => '연동사를 찾을 수 없습니다.']; + if (!$before) { + return ['ok' => false, 'message' => '연동사를 찾을 수 없습니다.']; + } - $data = [ - 'name' => trim($input['name']), - 'is_active' => (int)$input['is_active'], - ]; + $input['code'] = (string) $before->code; // 수정 시 code 고정 + $data = $this->buildProviderPayload($input, false); $this->repo->updateProvider($id, $data); - $this->audit->log($actorAdminId, 'admin.sale_code.provider.update', 'api_provider', $id, (array)$before, array_merge((array)$before, $data), $ip, $ua); + $this->audit->log( + $actorAdminId, + 'admin.sale_code.provider.update', + 'api_provider', + $id, + (array) $before, + array_merge((array) $before, $data), + $ip, + $ua + ); return ['ok' => true, 'message' => '연동사가 수정되었습니다.']; }); + } catch (\InvalidArgumentException $e) { + return ['ok' => false, 'message' => $e->getMessage()]; } catch (\Throwable $e) { return ['ok' => false, 'message' => '수정 중 오류가 발생했습니다.']; } @@ -90,12 +94,11 @@ final class AdminSaleCodeService } $this->repo->deleteProvider($id); - $this->audit->log($actorAdminId, 'admin.sale_code.provider.delete', 'api_provider', $id, (array)$before, null, $ip, $ua); + $this->audit->log($actorAdminId, 'admin.sale_code.provider.delete', 'api_provider', $id, (array) $before, null, $ip, $ua); return ['ok' => true, 'message' => '삭제되었습니다.']; } - // --- Product Code --- public function storeCode(array $input, int $actorAdminId, string $ip, string $ua): array { try { @@ -166,4 +169,86 @@ final class AdminSaleCodeService return ['ok' => true, 'message' => '상품 코드가 삭제되었습니다.']; } + + private function buildProviderPayload(array $input, bool $isCreate): array + { + $code = strtoupper(trim((string) ($input['code'] ?? ''))); + $transportType = strtoupper(trim((string) ($input['transport_type'] ?? ''))); + $baseUrl = $this->nullIfBlank($input['base_url'] ?? null); + $host = $this->nullIfBlank($input['host'] ?? null); + $port = isset($input['port']) && $input['port'] !== '' ? (int) $input['port'] : null; + + $configJson = $this->normalizeConfigJson($input['config_json'] ?? null, $code); + + if ($transportType === 'TCP_SOCKET') { + if ($host === null || $port === null) { + throw new \InvalidArgumentException('TCP_SOCKET 방식은 host, port를 반드시 입력해야 합니다.'); + } + } else { + if ($baseUrl === null) { + throw new \InvalidArgumentException('HTTP 방식은 base_url을 반드시 입력해야 합니다.'); + } + } + + return array_merge( + $isCreate ? ['code' => $code] : [], + [ + 'name' => trim((string) $input['name']), + 'transport_type' => $transportType, + 'base_url' => $baseUrl, + 'host' => $host, + 'port' => $port, + 'timeout_connect_sec' => (int) $input['timeout_connect_sec'], + 'timeout_read_sec' => (int) $input['timeout_read_sec'], + 'charset' => strtoupper(trim((string) $input['charset'])), + 'response_format_default' => $this->nullIfBlank($input['response_format_default'] ?? null), + 'is_test_mode' => (int) $input['is_test_mode'], + 'supports_issue' => (int) $input['supports_issue'], + 'supports_cancel' => (int) $input['supports_cancel'], + 'supports_resend' => (int) $input['supports_resend'], + 'supports_cancel_check' => (int) $input['supports_cancel_check'], + 'supports_network_cancel' => (int) $input['supports_network_cancel'], + 'config_json' => $configJson, + 'is_active' => (int) $input['is_active'], + 'sort_order' => 0, + ] + ); + } + + private function normalizeConfigJson(?string $json, string $providerCode): ?string + { + $json = $this->nullIfBlank($json); + if ($json === null) { + return null; + } + + $decoded = json_decode($json, true); + if (!is_array($decoded)) { + throw new \InvalidArgumentException('상세 설정 JSON 형식이 올바르지 않습니다.'); + } + + foreach ($this->requiredConfigKeys($providerCode) as $requiredKey) { + if (!array_key_exists($requiredKey, $decoded)) { + throw new \InvalidArgumentException("상세 설정 JSON에 필수 키가 없습니다: {$requiredKey}"); + } + } + + return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + private function requiredConfigKeys(string $providerCode): array + { + return match ($providerCode) { + 'DANAL' => ['cp_id', 'cp_pwd_enc', 'subcpid', 'crypto_key_enc', 'crypto_iv_enc'], + 'KORCULTURE' => ['member_code', 'sub_member_code', 'issue_tr_code', 'cancel_tr_code'], + 'KPREPAID' => ['chain_code', 'issue_path', 'cancel_path'], + default => [], + }; + } + + private function nullIfBlank(mixed $value): ?string + { + $value = is_string($value) ? trim($value) : $value; + return ($value === '' || $value === null) ? null : (string) $value; + } } diff --git a/resources/views/admin/product/salecode/index.blade.php b/resources/views/admin/product/salecode/index.blade.php index 1f16458..4e549a4 100644 --- a/resources/views/admin/product/salecode/index.blade.php +++ b/resources/views/admin/product/salecode/index.blade.php @@ -128,17 +128,97 @@ +
- +
+
- - + + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + + + + +
+ +
+ + +
업체별 인증값/경로/전문 기본값은 JSON으로 저장합니다.
+
+
@@ -154,7 +234,6 @@