feat:기업 기본정보 및 국세청 사업자등록 상태 조회 기능 추가
- CooconService에 OA08 기업기본정보 API 추가 - NtsBusinessService 신규 생성 (국세청 사업자등록 상태조회) - CreditInquiry 모델에 회사정보 및 국세청 상태 필드 추가 - 마이그레이션: 기업정보 및 국세청 상태 컬럼 추가 - UI: 리스트에 업체정보/국세청 상태 컬럼 표시 - 원본 데이터 모달에 회사정보 헤더 추가 - 리포트 모달에 회사정보 및 신용요약 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
208
app/Services/Nts/NtsBusinessService.php
Normal file
208
app/Services/Nts/NtsBusinessService.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user