feat:기업 기본정보 및 국세청 사업자등록 상태 조회 기능 추가

- CooconService에 OA08 기업기본정보 API 추가
- NtsBusinessService 신규 생성 (국세청 사업자등록 상태조회)
- CreditInquiry 모델에 회사정보 및 국세청 상태 필드 추가
- 마이그레이션: 기업정보 및 국세청 상태 컬럼 추가
- UI: 리스트에 업체정보/국세청 상태 컬럼 표시
- 원본 데이터 모달에 회사정보 헤더 추가
- 리포트 모달에 회사정보 및 신용요약 표시

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-22 20:34:43 +09:00
parent 0fe84fdebe
commit d3aeb498cb
6 changed files with 630 additions and 34 deletions

View File

@@ -6,6 +6,7 @@
use App\Models\Coocon\CooconConfig;
use App\Models\Credit\CreditInquiry;
use App\Services\Coocon\CooconService;
use App\Services\Nts\NtsBusinessService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -78,31 +79,47 @@ public function search(Request $request): JsonResponse
$companyKey = preg_replace('/[^0-9]/', '', $request->input('company_key'));
$service = new CooconService();
$cooconService = new CooconService();
if (!$service->hasConfig()) {
if (!$cooconService->hasConfig()) {
return response()->json([
'success' => false,
'error' => '쿠콘 API 설정이 없습니다. 설정을 먼저 등록해주세요.',
], 400);
}
// 전체 신용정보 조회
$apiResult = $service->getAllCreditInfo($companyKey);
// 전체 신용정보 조회 (쿠콘 API)
$apiResult = $cooconService->getAllCreditInfo($companyKey);
// 국세청 사업자등록 상태 조회
$ntsService = new NtsBusinessService();
$ntsResult = $ntsService->getBusinessStatus($companyKey);
// DB에 저장
$inquiry = CreditInquiry::createFromApiResponse(
$companyKey,
$apiResult,
$ntsResult,
auth()->id()
);
return response()->json([
'success' => true,
'data' => $apiResult,
'nts' => $ntsResult,
'inquiry_key' => $inquiry->inquiry_key,
'inquiry_id' => $inquiry->id,
'company_key' => $companyKey,
'company_info' => [
'company_name' => $inquiry->company_name,
'ceo_name' => $inquiry->ceo_name,
'company_address' => $inquiry->company_address,
'business_type' => $inquiry->business_type,
'business_item' => $inquiry->business_item,
'nts_status' => $inquiry->nts_status,
'nts_status_label' => $inquiry->nts_status_label,
'nts_tax_type' => $inquiry->nts_tax_type,
],
]);
}
@@ -122,6 +139,13 @@ public function getRawData(string $inquiryKey): JsonResponse
'company_key' => $inquiry->company_key,
'formatted_company_key' => $inquiry->formatted_company_key,
'company_name' => $inquiry->company_name,
'ceo_name' => $inquiry->ceo_name,
'company_address' => $inquiry->company_address,
'business_type' => $inquiry->business_type,
'business_item' => $inquiry->business_item,
'nts_status' => $inquiry->nts_status,
'nts_status_label' => $inquiry->nts_status_label,
'nts_tax_type' => $inquiry->nts_tax_type,
'inquired_at' => $inquiry->inquired_at->format('Y-m-d H:i:s'),
'status' => $inquiry->status,
'total_issue_count' => $inquiry->total_issue_count,
@@ -164,8 +188,20 @@ private function transformToReportFormat(CreditInquiry $inquiry): array
'company_info' => [
'company_key' => $inquiry->formatted_company_key,
'company_name' => $inquiry->company_name ?? '-',
'ceo_name' => $inquiry->ceo_name ?? '-',
'company_address' => $inquiry->company_address ?? '-',
'business_type' => $inquiry->business_type ?? '-',
'business_item' => $inquiry->business_item ?? '-',
'establishment_date' => $inquiry->establishment_date?->format('Y-m-d') ?? '-',
'inquired_at' => $inquiry->inquired_at->format('Y년 m월 d일 H:i'),
],
'nts_info' => [
'status' => $inquiry->nts_status ?? '-',
'status_label' => $inquiry->nts_status_label,
'tax_type' => $inquiry->nts_tax_type ?? '-',
'closure_date' => $inquiry->nts_closure_date?->format('Y-m-d') ?? null,
'is_active' => $inquiry->isNtsActive(),
],
'summary' => [
'total_issue_count' => $inquiry->total_issue_count,
'has_issue' => $inquiry->has_issue,

View File

@@ -15,6 +15,15 @@ class CreditInquiry extends Model
'inquiry_key',
'company_key',
'company_name',
'ceo_name',
'company_address',
'business_type',
'business_item',
'establishment_date',
'nts_status',
'nts_status_code',
'nts_tax_type',
'nts_closure_date',
'user_id',
'inquired_at',
'short_term_overdue_cnt',
@@ -29,18 +38,24 @@ class CreditInquiry extends Model
'raw_negative_info_cb',
'raw_suspension_info',
'raw_workout_info',
'raw_company_info',
'raw_nts_status',
'status',
'error_message',
];
protected $casts = [
'inquired_at' => 'datetime',
'establishment_date' => 'date',
'nts_closure_date' => 'date',
'raw_summary' => 'array',
'raw_short_term_overdue' => 'array',
'raw_negative_info_kci' => 'array',
'raw_negative_info_cb' => 'array',
'raw_suspension_info' => 'array',
'raw_workout_info' => 'array',
'raw_company_info' => 'array',
'raw_nts_status' => 'array',
'short_term_overdue_cnt' => 'integer',
'negative_info_kci_cnt' => 'integer',
'negative_info_pb_cnt' => 'integer',
@@ -121,6 +136,8 @@ public function getFormattedCompanyKeyAttribute(): string
public function getAllRawData(): array
{
return [
'companyInfo' => $this->raw_company_info,
'ntsStatus' => $this->raw_nts_status,
'summary' => $this->raw_summary,
'shortTermOverdue' => $this->raw_short_term_overdue,
'negativeInfoKCI' => $this->raw_negative_info_kci,
@@ -131,22 +148,82 @@ public function getAllRawData(): array
}
/**
* API 응답으로부터 모델 생성
* 국세청 상태가 정상(영업중)인지 확인
*/
public static function createFromApiResponse(string $companyKey, array $apiResult, ?int $userId = null): self
public function isNtsActive(): bool
{
return $this->nts_status_code === '01';
}
/**
* 국세청 상태 라벨
*/
public function getNtsStatusLabelAttribute(): string
{
return match ($this->nts_status_code) {
'01' => '계속사업자',
'02' => '휴업자',
'03' => '폐업자',
default => $this->nts_status ?: '미확인',
};
}
/**
* API 응답으로부터 모델 생성
*
* @param string $companyKey 사업자번호
* @param array $apiResult 쿠콘 API 결과
* @param array|null $ntsResult 국세청 API 결과
* @param int|null $userId 조회자 ID
*/
public static function createFromApiResponse(
string $companyKey,
array $apiResult,
?array $ntsResult = null,
?int $userId = null
): self {
// 요약 정보에서 건수 추출
$summaryData = $apiResult['summary']['data'] ?? [];
$creditSummaryList = $summaryData['data']['creditSummaryList'][0]
?? $summaryData['creditSummaryList'][0]
?? [];
// 기업 기본정보 추출 (OA08)
$companyInfoData = $apiResult['companyInfo']['data']['data'] ?? $apiResult['companyInfo']['data'] ?? [];
$companyName = $companyInfoData['korentrnm'] ?? $companyInfoData['entrpNm'] ?? null;
$ceoName = $companyInfoData['korreprnm'] ?? $companyInfoData['reprNm'] ?? null;
$companyAddress = $companyInfoData['addr'] ?? $companyInfoData['address'] ?? null;
$businessType = $companyInfoData['bizcnd'] ?? $companyInfoData['indutyNm'] ?? null;
$businessItem = $companyInfoData['bizitm'] ?? $companyInfoData['indutyDetailNm'] ?? null;
$establishmentDate = null;
if (!empty($companyInfoData['estbDt']) || !empty($companyInfoData['estbDate'])) {
$estbDt = $companyInfoData['estbDt'] ?? $companyInfoData['estbDate'];
if (strlen($estbDt) === 8) {
$establishmentDate = substr($estbDt, 0, 4) . '-' . substr($estbDt, 4, 2) . '-' . substr($estbDt, 6, 2);
}
}
// 국세청 상태 추출
$ntsStatus = null;
$ntsStatusCode = null;
$ntsTaxType = null;
$ntsClosureDate = null;
if ($ntsResult && ($ntsResult['success'] ?? false)) {
$ntsData = $ntsResult['data'] ?? [];
$ntsStatus = $ntsData['b_stt'] ?? null;
$ntsStatusCode = $ntsData['b_stt_cd'] ?? null;
$ntsTaxType = $ntsData['tax_type'] ?? null;
if (!empty($ntsData['end_dt']) && strlen($ntsData['end_dt']) === 8) {
$ntsClosureDate = substr($ntsData['end_dt'], 0, 4) . '-' . substr($ntsData['end_dt'], 4, 2) . '-' . substr($ntsData['end_dt'], 6, 2);
}
}
// 성공/실패 상태 판단
$successCount = 0;
$totalCount = 6;
$totalCount = 7; // companyInfo 추가
$errors = [];
foreach (['summary', 'shortTermOverdue', 'negativeInfoKCI', 'negativeInfoCB', 'suspensionInfo', 'workoutInfo'] as $key) {
foreach (['companyInfo', 'summary', 'shortTermOverdue', 'negativeInfoKCI', 'negativeInfoCB', 'suspensionInfo', 'workoutInfo'] as $key) {
if (isset($apiResult[$key]['success']) && $apiResult[$key]['success']) {
$successCount++;
} else {
@@ -165,6 +242,20 @@ public static function createFromApiResponse(string $companyKey, array $apiResul
'user_id' => $userId,
'inquired_at' => now(),
// 기업 기본정보
'company_name' => $companyName,
'ceo_name' => $ceoName,
'company_address' => $companyAddress,
'business_type' => $businessType,
'business_item' => $businessItem,
'establishment_date' => $establishmentDate,
// 국세청 상태
'nts_status' => $ntsStatus,
'nts_status_code' => $ntsStatusCode,
'nts_tax_type' => $ntsTaxType,
'nts_closure_date' => $ntsClosureDate,
// 요약 건수
'short_term_overdue_cnt' => $creditSummaryList['shorttermOverdueCnt'] ?? 0,
'negative_info_kci_cnt' => $creditSummaryList['negativeInfoBbCnt'] ?? 0,
@@ -174,6 +265,8 @@ public static function createFromApiResponse(string $companyKey, array $apiResul
'workout_cnt' => $creditSummaryList['workoutCnt'] ?? 0,
// 원본 데이터
'raw_company_info' => $apiResult['companyInfo'] ?? null,
'raw_nts_status' => $ntsResult,
'raw_summary' => $apiResult['summary'] ?? null,
'raw_short_term_overdue' => $apiResult['shortTermOverdue'] ?? null,
'raw_negative_info_kci' => $apiResult['negativeInfoKCI'] ?? null,

View File

@@ -18,6 +18,7 @@ class CooconService
/**
* API ID 상수
*/
public const API_COMPANY_INFO = 'OA08'; // 기업 기본정보
public const API_CREDIT_SUMMARY = 'OA12'; // 신용요약정보
public const API_SHORT_TERM_OVERDUE = 'OA13'; // 단기연체정보 (한국신용정보원)
public const API_NEGATIVE_INFO_KCI = 'OA14'; // 신용도판단정보 (한국신용정보원)
@@ -29,6 +30,7 @@ class CooconService
* API 이름 목록
*/
public const API_NAMES = [
self::API_COMPANY_INFO => '기업 기본정보',
self::API_CREDIT_SUMMARY => '신용요약정보',
self::API_SHORT_TERM_OVERDUE => '단기연체정보 (한국신용정보원)',
self::API_NEGATIVE_INFO_KCI => '신용도판단정보 (한국신용정보원)',
@@ -198,6 +200,20 @@ private function generateTransactionSequence(): string
return date('YmdHis') . substr(microtime(), 2, 6);
}
/**
* 기업 기본정보 조회 (OA08)
*
* @param string $companyKey 사업자번호, 법인번호, 업체코드 중 하나
* @param string $idscdcg 식별자구분코드 (기본값: 09-사업자등록번호)
*/
public function getCompanyInfo(string $companyKey, string $idscdcg = '09'): array
{
return $this->callApi(self::API_COMPANY_INFO, [
'Companykey' => $companyKey,
'idscdcg' => $idscdcg,
]);
}
/**
* 신용요약정보 조회 (OA12)
*
@@ -288,6 +304,9 @@ public function getAllCreditInfo(string $companyKey): array
{
$results = [];
// 기업 기본정보
$results['companyInfo'] = $this->getCompanyInfo($companyKey);
// 신용요약정보
$results['summary'] = $this->getCreditSummary($companyKey);

View File

@@ -0,0 +1,208 @@
<?php
namespace App\Services\Nts;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* 국세청 사업자등록 상태조회 서비스
* 공공데이터포털 API 사용
*/
class NtsBusinessService
{
/**
* API URL
*/
private const API_URL = 'https://api.odcloud.kr/api/nts-businessman/v1/status';
/**
* 서비스 키 (공공데이터포털에서 발급)
*/
private string $serviceKey;
public function __construct()
{
$this->serviceKey = config('services.nts.service_key', 'EFI7Fchltxh8LNyMu%2BUE9GSklj4ZsJqpL1UAYP6S0ci9D7fqJA98RRdxJos8KxwwEr6L9GAuAEB6E9IA1v1j2Q%3D%3D');
}
/**
* 사업자등록 상태 조회
*
* @param string $businessNumber 사업자등록번호 (10자리, 하이픈 없이)
* @return array
*/
public function getBusinessStatus(string $businessNumber): array
{
// 사업자번호에서 숫자만 추출
$bizNo = preg_replace('/[^0-9]/', '', $businessNumber);
if (strlen($bizNo) !== 10) {
return [
'success' => false,
'error' => '사업자등록번호는 10자리여야 합니다.',
'code' => 'INVALID_FORMAT',
];
}
$url = self::API_URL . '?serviceKey=' . $this->serviceKey;
Log::info('국세청 사업자등록 상태 조회', [
'biz_no' => $bizNo,
]);
try {
$response = Http::timeout(30)
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post($url, [
'b_no' => [$bizNo],
]);
$result = $response->json();
Log::info('국세청 API 응답', [
'biz_no' => $bizNo,
'match_cnt' => $result['match_cnt'] ?? 0,
'status_code' => $response->status(),
]);
if (!$response->successful()) {
return [
'success' => false,
'error' => 'HTTP 오류: ' . $response->status(),
'code' => 'HTTP_ERROR',
'http_status' => $response->status(),
];
}
$matchCnt = $result['match_cnt'] ?? 0;
if ($matchCnt >= 1 && isset($result['data'][0])) {
$data = $result['data'][0];
return [
'success' => true,
'data' => [
'b_no' => $data['b_no'] ?? $bizNo,
'b_stt' => $data['b_stt'] ?? '', // 상태 (영업, 휴업, 폐업)
'b_stt_cd' => $data['b_stt_cd'] ?? '', // 상태코드 (01: 계속사업자, 02: 휴업자, 03: 폐업자)
'tax_type' => $data['tax_type'] ?? '', // 과세유형
'tax_type_cd' => $data['tax_type_cd'] ?? '', // 과세유형코드
'end_dt' => $data['end_dt'] ?? '', // 폐업일
'utcc_yn' => $data['utcc_yn'] ?? '', // 단위과세전환여부
'tax_type_change_dt' => $data['tax_type_change_dt'] ?? '', // 과세유형전환일
'invoice_apply_dt' => $data['invoice_apply_dt'] ?? '', // 세금계산서적용일
'rbf_tax_type' => $data['rbf_tax_type'] ?? '', // 직전과세유형
'rbf_tax_type_cd' => $data['rbf_tax_type_cd'] ?? '', // 직전과세유형코드
],
'raw' => $result,
];
}
// 조회 실패 (국세청에 등록되지 않은 사업자번호)
$errorMsg = $result['data'][0]['tax_type'] ?? '조회된 결과가 없습니다.';
return [
'success' => false,
'error' => $errorMsg,
'code' => 'NOT_FOUND',
'raw' => $result,
];
} catch (\Exception $e) {
Log::error('국세청 API 호출 실패', [
'biz_no' => $bizNo,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'error' => '국세청 API 호출 중 오류가 발생했습니다: ' . $e->getMessage(),
'code' => 'EXCEPTION',
];
}
}
/**
* 여러 사업자번호 일괄 상태 조회
*
* @param array $businessNumbers 사업자등록번호 배열
* @return array
*/
public function getBusinessStatusBulk(array $businessNumbers): array
{
$bizNos = array_map(function ($num) {
return preg_replace('/[^0-9]/', '', $num);
}, $businessNumbers);
// 유효하지 않은 번호 필터링
$validBizNos = array_filter($bizNos, fn($num) => strlen($num) === 10);
if (empty($validBizNos)) {
return [
'success' => false,
'error' => '유효한 사업자등록번호가 없습니다.',
'code' => 'INVALID_FORMAT',
];
}
$url = self::API_URL . '?serviceKey=' . $this->serviceKey;
try {
$response = Http::timeout(30)
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post($url, [
'b_no' => array_values($validBizNos),
]);
$result = $response->json();
if (!$response->successful()) {
return [
'success' => false,
'error' => 'HTTP 오류: ' . $response->status(),
'code' => 'HTTP_ERROR',
];
}
return [
'success' => true,
'data' => $result['data'] ?? [],
'match_cnt' => $result['match_cnt'] ?? 0,
'raw' => $result,
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => '국세청 API 호출 중 오류가 발생했습니다: ' . $e->getMessage(),
'code' => 'EXCEPTION',
];
}
}
/**
* 사업자 상태 코드를 한글로 변환
*/
public static function getStatusLabel(string $statusCode): string
{
return match ($statusCode) {
'01' => '계속사업자',
'02' => '휴업자',
'03' => '폐업자',
default => '알 수 없음',
};
}
/**
* 사업자 상태가 정상(영업중)인지 확인
*/
public static function isActiveStatus(string $statusCode): bool
{
return $statusCode === '01';
}
}

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('credit_inquiries', function (Blueprint $table) {
// 기업 기본정보 (OA08)
$table->string('ceo_name')->nullable()->after('company_name')->comment('대표자명');
$table->string('company_address')->nullable()->after('ceo_name')->comment('회사 주소');
$table->string('business_type')->nullable()->after('company_address')->comment('업종');
$table->string('business_item')->nullable()->after('business_type')->comment('업태');
$table->date('establishment_date')->nullable()->after('business_item')->comment('설립일');
// 국세청 사업자등록 상태
$table->string('nts_status', 20)->nullable()->after('establishment_date')->comment('국세청 상태 (영업/휴업/폐업)');
$table->string('nts_status_code', 2)->nullable()->after('nts_status')->comment('국세청 상태코드 (01/02/03)');
$table->string('nts_tax_type', 50)->nullable()->after('nts_status_code')->comment('과세유형');
$table->date('nts_closure_date')->nullable()->after('nts_tax_type')->comment('폐업일');
// API 원본 데이터
$table->json('raw_company_info')->nullable()->after('raw_workout_info')->comment('OA08 기업기본정보 원본');
$table->json('raw_nts_status')->nullable()->after('raw_company_info')->comment('국세청 상태조회 원본');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('credit_inquiries', function (Blueprint $table) {
$table->dropColumn([
'ceo_name',
'company_address',
'business_type',
'business_item',
'establishment_date',
'nts_status',
'nts_status_code',
'nts_tax_type',
'nts_closure_date',
'raw_company_info',
'raw_nts_status',
]);
});
}
};

View File

@@ -98,11 +98,11 @@ class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<table class="w-full text-sm">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-4 py-3 text-left font-medium text-gray-700">조회키</th>
<th class="px-4 py-3 text-left font-medium text-gray-700">사업자번호</th>
<th class="px-4 py-3 text-left font-medium text-gray-700">업체</th>
<th class="px-4 py-3 text-left font-medium text-gray-700">업체정보</th>
<th class="px-4 py-3 text-center font-medium text-gray-700">국세청</th>
<th class="px-4 py-3 text-left font-medium text-gray-700">조회일시</th>
<th class="px-4 py-3 text-center font-medium text-gray-700">상태</th>
<th class="px-4 py-3 text-center font-medium text-gray-700">API상태</th>
<th class="px-4 py-3 text-center font-medium text-gray-700">이슈</th>
<th class="px-4 py-3 text-left font-medium text-gray-700">조회자</th>
<th class="px-4 py-3 text-right font-medium text-gray-700">액션</th>
@@ -111,9 +111,24 @@ class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<tbody class="divide-y divide-gray-100">
@forelse($inquiries as $inquiry)
<tr class="hover:bg-gray-50" data-inquiry-key="{{ $inquiry->inquiry_key }}">
<td class="px-4 py-3 font-mono text-xs text-gray-500">{{ substr($inquiry->inquiry_key, 0, 16) }}...</td>
<td class="px-4 py-3 font-mono font-medium">{{ $inquiry->formatted_company_key }}</td>
<td class="px-4 py-3">{{ $inquiry->company_name ?? '-' }}</td>
<td class="px-4 py-3">
<div class="text-sm font-medium text-gray-900">{{ $inquiry->company_name ?? '-' }}</div>
@if($inquiry->ceo_name)
<div class="text-xs text-gray-500">대표: {{ $inquiry->ceo_name }}</div>
@endif
</td>
<td class="px-4 py-3 text-center">
@if($inquiry->nts_status_code === '01')
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded">{{ $inquiry->nts_status_label }}</span>
@elseif($inquiry->nts_status_code === '02')
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded">{{ $inquiry->nts_status_label }}</span>
@elseif($inquiry->nts_status_code === '03')
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded">{{ $inquiry->nts_status_label }}</span>
@else
<span class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-500 rounded">{{ $inquiry->nts_status_label }}</span>
@endif
</td>
<td class="px-4 py-3 text-gray-600">{{ $inquiry->inquired_at->format('Y-m-d H:i') }}</td>
<td class="px-4 py-3 text-center">
@if($inquiry->status === 'success')
@@ -478,8 +493,8 @@ class="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transitio
// 원본 데이터 패널 렌더링 (기존 종합 패널과 동일)
function renderRawDataPanel(data, inquiry) {
// 이전에 구현한 종합 패널 렌더링 로직을 그대로 사용
return renderComprehensivePanel(data, inquiry.company_key);
// inquiry 객체에서 회사 정보 포함해서 렌더링
return renderComprehensivePanel(data, inquiry);
}
// 종합 패널 렌더링 함수들 (기존 코드와 동일)
@@ -496,20 +511,63 @@ function renderRawDataPanel(data, inquiry) {
'3': '법인등록번호',
};
function renderComprehensivePanel(data, companyKey) {
function renderComprehensivePanel(data, inquiry) {
let html = '';
const companyKey = inquiry.company_key || inquiry;
// 헤더
// 회사 정보 헤더
html += `
<div class="mb-6 pb-4 border-b">
<h2 class="text-lg font-bold text-gray-800">신용도판단 종합 조회 결과</h2>
<p class="text-sm text-gray-500">사업자/법인번호: <span class="font-mono font-medium">${formatBusinessNumber(companyKey)}</span></p>
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<div class="flex-1">
<h2 class="text-lg font-bold text-gray-800 mb-2">
${inquiry.company_name || '업체명 미확인'}
${inquiry.nts_status_label ? renderNtsStatusBadge(inquiry.nts_status_code, inquiry.nts_status_label) : ''}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1 text-sm">
<div class="flex gap-2">
<span class="text-gray-500 w-20">사업자번호</span>
<span class="font-mono font-medium">${formatBusinessNumber(companyKey)}</span>
</div>
${inquiry.ceo_name ? `
<div class="flex gap-2">
<span class="text-gray-500 w-20">대표자</span>
<span>${inquiry.ceo_name}</span>
</div>` : ''}
${inquiry.company_address ? `
<div class="flex gap-2 md:col-span-2">
<span class="text-gray-500 w-20">주소</span>
<span class="text-gray-700">${inquiry.company_address}</span>
</div>` : ''}
${inquiry.business_type || inquiry.business_item ? `
<div class="flex gap-2">
<span class="text-gray-500 w-20">업종/업태</span>
<span class="text-gray-700">${[inquiry.business_type, inquiry.business_item].filter(Boolean).join(' / ')}</span>
</div>` : ''}
${inquiry.nts_tax_type ? `
<div class="flex gap-2">
<span class="text-gray-500 w-20">과세유형</span>
<span class="text-gray-700">${inquiry.nts_tax_type}</span>
</div>` : ''}
</div>
</div>
</div>
</div>
`;
// 신용요약
html += renderSummaryCard(data.summary);
// 기업기본정보 섹션 (OA08)
if (data.companyInfo) {
html += renderSection('기업 기본정보', 'NICE', 'OA08', data.companyInfo, renderCompanyInfoDetail);
}
// 국세청 상태 섹션
if (data.ntsStatus) {
html += renderSection('사업자등록 상태', '국세청', 'NTS', { success: data.ntsStatus?.success, data: data.ntsStatus }, renderNtsStatusDetail);
}
// 각 섹션
html += renderSection('단기연체정보', '한국신용정보원', 'OA13', data.shortTermOverdue, renderShortTermOverdue);
html += renderSection('신용도판단정보', '한국신용정보원', 'OA14', data.negativeInfoKCI, renderNegativeInfoKCI);
@@ -520,6 +578,54 @@ function renderComprehensivePanel(data, companyKey) {
return html;
}
function renderNtsStatusBadge(statusCode, statusLabel) {
const colors = {
'01': 'bg-green-100 text-green-700',
'02': 'bg-yellow-100 text-yellow-700',
'03': 'bg-red-100 text-red-700',
};
const colorClass = colors[statusCode] || 'bg-gray-100 text-gray-600';
return `<span class="ml-2 px-2 py-0.5 text-xs font-medium ${colorClass} rounded">${statusLabel}</span>`;
}
function renderCompanyInfoDetail(data) {
const info = data?.data || data || {};
if (!info || Object.keys(info).length === 0) {
return renderNoData('기업 기본정보가 없습니다.');
}
let html = '<div class="grid grid-cols-2 gap-4 text-sm">';
const fields = [
['korentrnm', '업체명'], ['korreprnm', '대표자명'], ['bizno', '사업자번호'],
['crpno', '법인번호'], ['addr', '주소'], ['bizcnd', '업종'], ['bizitm', '업태'],
['estbDt', '설립일'], ['empCnt', '종업원수'], ['salesAmt', '매출액']
];
fields.forEach(([key, label]) => {
if (info[key]) {
html += `<div><span class="text-gray-500">${label}:</span> <span class="font-medium">${info[key]}</span></div>`;
}
});
html += '</div>';
return html;
}
function renderNtsStatusDetail(data) {
const nts = data?.data || {};
if (!nts || Object.keys(nts).length === 0) {
return renderNoData('국세청 사업자등록 상태 정보가 없습니다.');
}
let html = '<div class="grid grid-cols-2 gap-4 text-sm">';
html += `<div><span class="text-gray-500">사업자번호:</span> <span class="font-mono font-medium">${nts.b_no || '-'}</span></div>`;
html += `<div><span class="text-gray-500">상태:</span> <span class="font-medium">${nts.b_stt || '-'}</span></div>`;
html += `<div><span class="text-gray-500">과세유형:</span> <span class="font-medium">${nts.tax_type || '-'}</span></div>`;
if (nts.end_dt) {
html += `<div><span class="text-gray-500">폐업일:</span> <span class="font-medium text-red-600">${nts.end_dt}</span></div>`;
}
html += '</div>';
return html;
}
function renderSummaryCard(result) {
if (!result || !result.success) {
return `<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-4"><p class="text-red-700">신용요약정보 조회 실패</p></div>`;
@@ -663,25 +769,104 @@ function renderWorkoutInfo(data) {
return html;
}
// 리포트 패널 렌더링 (sales.sam.kr 형태로 - 추후 구현)
// 리포트 패널 렌더링
function renderReportPanel(data) {
// 임시 구현 - sales.sam.kr 형태 확인 후 수정 예정
const company = data.company_info || {};
const nts = data.nts_info || {};
const summary = data.summary || {};
const hasIssue = summary.has_issue;
return `
<div class="text-center py-8">
<p class="text-gray-500 mb-4">리포트 형식은 추후 구현 예정입니다.</p>
<p class="text-sm text-gray-400">sales.sam.kr/creditreport/index.php 형태의 코드가 필요합니다.</p>
<div class="mt-6 text-left bg-gray-50 rounded-lg p-4">
<h4 class="font-medium mb-2">조회 요약</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">총 이슈 건수:</span> <span class="font-medium">${data.summary?.total_issue_count || 0}건</span></div>
<div><span class="text-gray-500">단기연체:</span> <span class="font-medium">${data.summary?.short_term_overdue_cnt || 0}건</span></div>
<div><span class="text-gray-500">신용도판단(KCI):</span> <span class="font-medium">${data.summary?.negative_info_kci_cnt || 0}건</span></div>
<div><span class="text-gray-500">공공정보:</span> <span class="font-medium">${data.summary?.negative_info_pb_cnt || 0}건</span></div>
<div><span class="text-gray-500">신용도판단(CB):</span> <span class="font-medium">${data.summary?.negative_info_cb_cnt || 0}건</span></div>
<div><span class="text-gray-500">당좌정지:</span> <span class="font-medium">${data.summary?.suspension_info_cnt || 0}건</span></div>
<div><span class="text-gray-500">법정관리:</span> <span class="font-medium">${data.summary?.workout_cnt || 0}건</span></div>
<div class="report-content">
<!-- 회사 정보 헤더 -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-6">
<div class="flex items-start justify-between">
<div>
<h2 class="text-2xl font-bold text-gray-800 mb-1">
${company.company_name || '업체명 미확인'}
</h2>
<p class="text-lg font-mono text-gray-600">${company.company_key || '-'}</p>
</div>
<div class="text-right">
${renderNtsStatusBadgeLarge(nts.status_label, nts.is_active)}
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
${company.ceo_name ? `<div><span class="text-gray-500">대표자:</span> <span class="font-medium">${company.ceo_name}</span></div>` : ''}
${company.company_address && company.company_address !== '-' ? `<div class="md:col-span-2"><span class="text-gray-500">주소:</span> <span>${company.company_address}</span></div>` : ''}
${company.business_type && company.business_type !== '-' ? `<div><span class="text-gray-500">업종:</span> <span>${company.business_type}</span></div>` : ''}
${company.business_item && company.business_item !== '-' ? `<div><span class="text-gray-500">업태:</span> <span>${company.business_item}</span></div>` : ''}
${company.establishment_date && company.establishment_date !== '-' ? `<div><span class="text-gray-500">설립일:</span> <span>${company.establishment_date}</span></div>` : ''}
${nts.tax_type && nts.tax_type !== '-' ? `<div><span class="text-gray-500">과세유형:</span> <span>${nts.tax_type}</span></div>` : ''}
</div>
<div class="mt-3 text-xs text-gray-400">
조회일시: ${company.inquired_at || '-'}
</div>
</div>
<!-- 신용 요약 -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3">신용 정보 요약</h3>
<div class="${hasIssue ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200'} border rounded-lg p-4 mb-4">
<div class="flex items-center gap-3">
${hasIssue
? '<svg class="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>'
: '<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
}
<div>
<p class="${hasIssue ? 'text-red-700' : 'text-green-700'} font-bold text-lg">
${hasIssue ? ` ${summary.total_issue_count}건의 신용 이슈가 발견되었습니다.` : '신용 이슈가 없습니다.'}
</p>
<p class="${hasIssue ? 'text-red-600' : 'text-green-600'} text-sm">
${hasIssue ? '상세 내용을 확인하시기 바랍니다.' : '깨끗한 신용 상태입니다.'}
</p>
</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
${renderReportSummaryItem('단기연체', summary.short_term_overdue_cnt)}
${renderReportSummaryItem('신용도판단(KCI)', summary.negative_info_kci_cnt)}
${renderReportSummaryItem('공공정보', summary.negative_info_pb_cnt)}
${renderReportSummaryItem('신용도판단(CB)', summary.negative_info_cb_cnt)}
${renderReportSummaryItem('당좌정지', summary.suspension_info_cnt)}
${renderReportSummaryItem('법정관리', summary.workout_cnt)}
</div>
</div>
<!-- 국세청 정보 -->
${nts.closure_date ? `
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div>
<p class="text-red-700 font-bold">폐업 사업자입니다</p>
<p class="text-red-600 text-sm">폐업일: ${nts.closure_date}</p>
</div>
</div>
</div>
` : ''}
</div>
`;
}
function renderNtsStatusBadgeLarge(statusLabel, isActive) {
if (!statusLabel) return '';
const colorClass = isActive ? 'bg-green-100 text-green-800 border-green-300' : 'bg-red-100 text-red-800 border-red-300';
return `<span class="px-4 py-2 text-sm font-bold ${colorClass} border rounded-lg">${statusLabel}</span>`;
}
function renderReportSummaryItem(label, count) {
const hasIssue = count > 0;
const bgColor = hasIssue ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200';
const textColor = hasIssue ? 'text-red-700' : 'text-green-700';
return `
<div class="border rounded-lg p-3 text-center ${bgColor}">
<p class="text-xs ${textColor} mb-1">${label}</p>
<p class="text-2xl font-bold ${textColor}">${count || 0}</p>
<p class="text-xs ${textColor}">건</p>
</div>
`;
}