feat(WEB): 파비콘 변경 및 거래처 신용분석 모달 추가
- 파비콘: SVG 형식으로 변경 (white 로고 + 파란 배경) - 헤더 로고: SAM 로고 이미지로 교체 - 거래처 상세: 신용분석 모달 컴포넌트 추가 - 신용등급, 리스크 지표, 레이더 차트 - 프린트 기능 지원 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
103
claudedocs/[PLAN-2026-01-23] vendor-credit-analysis-modal.md
Normal file
103
claudedocs/[PLAN-2026-01-23] vendor-credit-analysis-modal.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 신규 거래처 신용분석 모달
|
||||
|
||||
## 개요
|
||||
- **목적**: 신규 거래처 등록 시 국가관리 API를 통해 받아온 기업 신용정보를 표시
|
||||
- **위치**: 거래처 등록 완료 후 모달로 표시
|
||||
- **현재 단계**: 목업 데이터로 UI 구현 (추후 API 연동)
|
||||
|
||||
## 화면 구성
|
||||
|
||||
### 1. 헤더
|
||||
- 로고 + "SAM 기업 신용분석 리포트"
|
||||
- 조회일시 표시
|
||||
|
||||
### 2. 기업 정보
|
||||
- "신규거래 신용정보 조회" 뱃지
|
||||
- "기업 신용 분석" 제목
|
||||
- 사업자번호, 법인명 (대표자명), 평가기준일 정보
|
||||
|
||||
### 3. 자료 효력기간 안내
|
||||
- 노란 배경의 알림 박스
|
||||
- 데이터 유효기간 및 면책 안내
|
||||
|
||||
### 4. 종합 신용 신호등
|
||||
- 5단계 신호등 표시 (Level 1~5)
|
||||
- 현재 레벨 강조 (예: 양호 Level 4)
|
||||
- 신용 등급 설명 텍스트
|
||||
- "유료 상세 분석 제공받기" 버튼
|
||||
|
||||
### 5. 신용 리스크 프로필
|
||||
- 오각형 레이더 차트
|
||||
- 한국신용평가등급
|
||||
- 금융 종합 위험도
|
||||
- 매입 결제
|
||||
- 매출 결제
|
||||
- 저당권설정
|
||||
|
||||
### 6. 신용 상세 정보
|
||||
- 신용채무정보 버튼
|
||||
- 신용등급추이정보 버튼
|
||||
- 정보 없음 안내 텍스트
|
||||
|
||||
### 7. 하단 거래 승인 판정
|
||||
- 안전/위험 배지
|
||||
- 신용등급 (Level 1~5)
|
||||
- 거래 유형 (계속사업자/신규거래 등)
|
||||
- 외상 가능 여부
|
||||
- "거래 승인 완료" 버튼
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
```typescript
|
||||
interface CreditAnalysisData {
|
||||
// 기업 정보
|
||||
businessNumber: string; // 사업자번호
|
||||
companyName: string; // 법인명
|
||||
representativeName: string; // 대표자명
|
||||
evaluationDate: string; // 평가기준일
|
||||
|
||||
// 신용 등급
|
||||
creditLevel: 1 | 2 | 3 | 4 | 5; // 1: 위험, 5: 최우량
|
||||
creditStatus: '위험' | '주의' | '보통' | '양호' | '우량';
|
||||
|
||||
// 리스크 프로필 (0~100)
|
||||
riskProfile: {
|
||||
koreaCreditRating: number; // 한국신용평가등급
|
||||
financialRisk: number; // 금융 종합 위험도
|
||||
purchasePayment: number; // 매입 결제
|
||||
salesPayment: number; // 매출 결제
|
||||
mortgageSetting: number; // 저당권설정
|
||||
};
|
||||
|
||||
// 거래 승인 판정
|
||||
approval: {
|
||||
safety: '안전' | '주의' | '위험';
|
||||
level: number;
|
||||
businessType: string; // 계속사업자, 신규거래 등
|
||||
creditAvailable: boolean; // 외상 가능 여부
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/components/accounting/VendorManagement/
|
||||
├── CreditAnalysisModal.tsx # 신용분석 모달 컴포넌트
|
||||
└── CreditAnalysisModal/
|
||||
├── index.tsx # 메인 모달
|
||||
├── CreditSignal.tsx # 신용 신호등 컴포넌트
|
||||
├── RiskRadarChart.tsx # 레이더 차트 컴포넌트
|
||||
└── types.ts # 타입 정의
|
||||
|
||||
src/app/[locale]/(protected)/dev/
|
||||
└── credit-analysis-test/
|
||||
└── page.tsx # 테스트 페이지
|
||||
```
|
||||
|
||||
## 구현 순서
|
||||
|
||||
1. [x] 계획 md 파일 작성
|
||||
2. [ ] CreditAnalysisModal 컴포넌트 생성
|
||||
3. [ ] 테스트 페이지 생성
|
||||
4. [ ] dev/test-urls에 URL 추가
|
||||
BIN
public/sam-logo.png
Normal file
BIN
public/sam-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
4
src/app/icon.svg
Normal file
4
src/app/icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 193 192">
|
||||
<rect width="193" height="192" rx="24" fill="#3B82F6"/>
|
||||
<image x="0" y="0" width="193" height="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMEAAADACAYAAAC9Hgc5AAAACXBIWXMAAAsSAAALEgHS3X78AAAI60lEQVR4nO3d7XHcRhZG4ddb/k9lYGYgbgSmI1hvBGpGIDmDdQZ2BIQiWCkCUxGsnIGUgRiB/APCckwOyQHQfT+6z1PlKpftImGODhsY3EF/9/XrVwEj+4f3AQDeiADDIwIMjwgwPCLA8IgAwyMCDI8IMDwiwPCIAMMjAgyPCDA8IvB1Ieln74MYHRH4uZB0I+m/kt74HsrYvmOU2sUSwNnBP3srqXgczOhYCewVPQxAkl5JmoyPBZK+9z6AwRRJ10/8+1cH/x2McDpkp+jpAA590HzB/KXZ0eD/OB2y8UanByBJP2o+ZXrR5GjwN6wE7U26O81Z609Jl2JFaIqVoK1J2wOQpJeaV4SLGgeD41gJ2pm0L4BDt5pXhI+Vvh4OEEF9LzT/9n5Z+esSQiOcDtXVKgBpvq9wI06NqiOCeloGsDiT9D9xH6EqIqjjQvNpSssADl2LEKrhjvF+x+aALCz3HSbj79sdVoJ9vAJYXEv6zel7d4MItruUbwCL12I12IUItimS/pB/AAsmUHcggvWK1s0BWXkl6Z2YN1qNm2XrFMUM4BDzRiuxEpxuUvwApLt5I1aEExHBaSbVmwOyQAgrEMHzJuUKYPFS0icxZvEsrgke90LzheaP3geyE4N3zyCC4yzmgCwRwhM4HXqotwCkuwnU4nsYMRHB352rvwAWZ2Lw7igG6O54zwFZYfDuHlaC2SgBLK4l/cf7IKLgwni8AA7x6EexEhTNn9QaMQCJwTtJY0dQlGMMorXhQxj1dKiIAO4bdvBuxJXgNxHAMcPOG422EkzKOQdkabgVYaSVYBIBnOKl5vGKYQbvRojghQhgrR800IO+ej8d6nEOyNIQg3c9rwQEsN8yeNf1Dpu9RkAA9Zxp3mGzOB9HMz1GcKH5E1UEUFe3E6i9RTDyHJCFLkPoKQICsHGtzsYseomgKGcAt5pvTmXT1bxRDxEUzb+dMgZw+e2vD65Hsk03IWSPoCjnHNDh++9fvv39W8fj2eqVOpg3yhzB2r2Bo/hTd5t6HCrKGUL6PZez3jGelHMM4pThtEn9/r+FlHElmNT3H5Ii6ar1wTSQds/lbBFMyhnAe637LTmJEMxkOR3KPAax58PsRTmve1IN3mVYCUYNQJpXhH9r/kOVSao9l6NHMHIAi3eaf6tmDCHFnsuRI7DeG7imK9V98T8qZwhSgnmjqBEsc0A/OB/HFldqcyd1CeFzg6/dWugQIkaQeRCuVQCL5bO/GeeNwu65HO3doUvN58DZArjV/OmrG6Pvx7VSRZFWgqJYewOfank78Mbwey7zRhlXhHCDd1EiKOL98LW+aD41yjhvFGrP5QgRFOUMYBmD8L4hVJQzhH8pyOCd9zXBpJxjEBGHxSbxs9zEcyWYxItWU5H0q/dBbOD+DFSvlWBSzgA+aH4XKFoAh4pynl66XV9ZrwTLW3sZA3irmCvAfZNyTqC6zRtZRrAEkHFz7HDvbT9j0hxCtjELlxCsIuDmjr1JOeeNzPdctojgXHkD+EU5A1hkHbwz3XO59YUxc0Ax8Do8oeVKwA8+jo+aV+SMYxbN91xutRJkDeBW8xL8zvk4WuHa7IgWK0FRzr2Bl/epew1AYvDuqNoRFHGjJrolhPfOx7FFkxBqng4V5Qzgs+a7wCMEcN+knDcuq46u1FoJsu4N/NgjEUdRlHMCteq8UY0IJkmvK3wda1EH4awVSb97H8QG1ULYG8EkltMevFHOeaMqey5vjSDz3sBZBuGsTcoZwu49l7dcGPNec9+Kcl7fbX6Hb+1KQAD9myT9UznnjW60Yc/lNRFkDuBXEcAamQfvVu+5vCaC829/ZXOlxrMnncoawmprIsj4Q+ltEM5axiferX7N114TZAnhVgRQyyflmTfa9JpvHZuIPCU60hyQpejXhJt/6W29TxB1RSCAdpbBu4h7Lu9a9ffcMY4WAgG0F3HP5d2nvXvHJqKEMPognLWiGCFUue6rNUrteY3AHJCfSX6jM9Xe+Kg1Su21IhCAryKfeaOq7/zV/GSZdQhr9wZGG5NsQ6j+1nftj1dahfBW8Z8JOpJJNiE0ufeT8WkTDMLFdal22201u/nZ6rlDrVaE30UAkd2ozeve9O5/y4dv1Q7hSvMnoForBt+jZy1e96nS1zqq9bNIa/1ALOeAiuH36lWtPZdNXneLB/LuDcFjEC7cDosJ7Z1ANXvdrR7NviWEW0k/ye8PIyHst/WJd6a/+Cw36VgTgsfewMcQwn5rQzBf+a23azolhGiDcISw36l7Lrt8BsRj98qnQoiyN/B9hFBH0eMhuH0IymsL12MhRA1gQQh1FD0MwfVTgJ77GB+GkGUQjhDqKJq3wpICfAzWe0d7aX6CxRfFCeBGz++wyehGHeeaP8Ps6nvvA1CAH8IGywx98TyIDnzyPgDJ93QoO06NOkEE+xBCB4hgP0JIjgjqIITEiKAeQkiKCOoihISIoD5CSIYI2iCERIigHUJIggjaIoQEiKA9QgiOCGwQQmBEYIcQgiICW4QQEBHYI4RgiMAHIQRCBH4IIQgi8EUIARCBP0JwRgQxEIIjIoiDEJwQQSyE4IAI4iEEY0QQEyEYIoK4CMEIEcRGCAaIID5CaIwIciCEhoggD0JohAhyIYQGiCAfQqiMCHIihIqIIC9CqIQIciOECojgoTc6bcPxKAhhJyJ46JQNx6MhhB2I4DhCGAgRPI4QBkEETyOEAUTYzDuDC82bfJ85H8caP2k+ZjyDleA02VaEKxHAyYjgdFlCuBKnQ6sQwTrRQyCADYhgvaghEMBGRLBNtBAIYAci2C5KCASwExHs4x0CAVRABPt5hUAAlRBBHdYhEEBFRFCPVQgEUBkR1NU6BAJogAjqaxUCATRCBG3UDoEAGiKCdmqFQACNEUFbe0MgAANE0N7WEAjACBHYWBsCARgiAjunhkAAxojA1nMhEIADIrD3WAgE4IQIfNwPgQAc8bQJXxeSziW9cz6OoREBhsfpEIZHBBgeEWB4RIDhEQGGRwQYHhFgeESA4REBhkcEGB4RYHhEgOH9BXhIfCzdDE+TAAAAAElFTkSuQmCC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CreditSignal } from './CreditSignal';
|
||||
import { RiskRadarChart } from './RiskRadarChart';
|
||||
import type { CreditAnalysisData } from './types';
|
||||
import { CREDIT_LEVEL_CONFIG } from './types';
|
||||
|
||||
interface CreditAnalysisDocumentProps {
|
||||
data: CreditAnalysisData;
|
||||
onApprove?: () => void;
|
||||
}
|
||||
|
||||
export function CreditAnalysisDocument({
|
||||
data,
|
||||
onApprove,
|
||||
}: CreditAnalysisDocumentProps) {
|
||||
const levelConfig = CREDIT_LEVEL_CONFIG[data.creditLevel];
|
||||
const [activeTab, setActiveTab] = useState('shortTermOverdue');
|
||||
|
||||
// 탭 콘텐츠 렌더링
|
||||
const renderTabContent = (content: string | null, emptyMessage: string) => {
|
||||
if (content) {
|
||||
return <p className="text-sm text-gray-700">{content}</p>;
|
||||
}
|
||||
return (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-gray-500">▲ {emptyMessage}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b pb-4 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-green-500 rounded flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">S</span>
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">
|
||||
SAM 기업 신용분석 리포트
|
||||
</h1>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{data.evaluationDate}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기업 정보 */}
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="mb-2 bg-blue-50 text-blue-600 border-blue-200">
|
||||
신규거래 신용정보 조회
|
||||
</Badge>
|
||||
<h2 className="text-xl font-bold text-gray-800 mt-2">
|
||||
- 기업 신용 분석 -
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
사업자번호: {data.businessNumber} | {data.companyName} | 거래기준일: {data.evaluationDate.split(' ')[0]}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
조회일자: {data.queryDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 자료 효력기간 안내 */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-yellow-800">자료 효력기간 안내</p>
|
||||
<p className="text-yellow-700 mt-1">
|
||||
본 보고서(신용분석 6개월 {data.evaluationDate}) 자료는 기준일 당시의 시점이며, 이후 발생한 신용상태의 변동은 반영되지 않았음을 알려드립니다. 신용분석 결과에 의한 거래 관련 판단 및 결정에 대해서는 본사에서 책임지지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 종합 신용 신호등 & 리스크 프로필 */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 종합 신용 신호등 */}
|
||||
<div className="bg-gray-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4 text-center">
|
||||
종합 신용 신호등
|
||||
</h3>
|
||||
<CreditSignal
|
||||
level={data.creditLevel}
|
||||
taxStatus={data.taxStatus}
|
||||
/>
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="outline" size="sm" className="text-blue-600 border-blue-300">
|
||||
유료 상세 분석 제공받기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 신용 리스크 프로필 */}
|
||||
<div className="bg-gray-50 rounded-lg p-6">
|
||||
<RiskRadarChart data={data.riskProfile} creditDetailInfo={data.creditDetailInfo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 신용 상세 정보 - 탭 */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4 text-center">
|
||||
신용 상세 정보
|
||||
</h3>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="shortTermOverdue" className="text-xs sm:text-sm">
|
||||
단기연체정보
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="creditJudgment" className="text-xs sm:text-sm">
|
||||
신용도판단정보
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="checkingSuspension" className="text-xs sm:text-sm">
|
||||
당좌거래정지
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="courtManagement" className="text-xs sm:text-sm">
|
||||
법정관리/워크아웃
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="shortTermOverdue" className="mt-4">
|
||||
{renderTabContent(data.creditDetailTab.shortTermOverdue, '단기연체 정보가 없습니다')}
|
||||
</TabsContent>
|
||||
<TabsContent value="creditJudgment" className="mt-4">
|
||||
{renderTabContent(data.creditDetailTab.creditJudgment, '신용도판단 정보가 없습니다')}
|
||||
</TabsContent>
|
||||
<TabsContent value="checkingSuspension" className="mt-4">
|
||||
{renderTabContent(data.creditDetailTab.checkingSuspension, '당좌거래정지 정보가 없습니다')}
|
||||
</TabsContent>
|
||||
<TabsContent value="courtManagement" className="mt-4">
|
||||
{renderTabContent(data.creditDetailTab.courtManagement, '법정관리/워크아웃 정보가 없습니다')}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 거래 승인 판정 */}
|
||||
<div className="bg-gray-100 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-6 flex-wrap">
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-600">거래 승인 판정</span>
|
||||
<div className="mt-1">
|
||||
<Badge
|
||||
className={cn(
|
||||
'text-base px-4 py-1',
|
||||
data.approval.safety === '안전'
|
||||
? 'bg-green-500 hover:bg-green-600'
|
||||
: data.approval.safety === '주의'
|
||||
? 'bg-yellow-500 hover:bg-yellow-600'
|
||||
: 'bg-red-500 hover:bg-red-600'
|
||||
)}
|
||||
>
|
||||
{data.approval.safety}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 border-l border-gray-300 hidden sm:block" />
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-600">신용등급</span>
|
||||
<p className={cn('font-bold', levelConfig.color)}>
|
||||
Level {data.approval.level}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 border-l border-gray-300 hidden sm:block" />
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-600">거래</span>
|
||||
<p className="font-medium text-gray-800">{data.approval.businessType}</p>
|
||||
</div>
|
||||
<div className="h-12 border-l border-gray-300 hidden sm:block" />
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-600">외상가능</span>
|
||||
<p className={cn(
|
||||
'font-medium',
|
||||
data.approval.creditAvailable ? 'text-green-600' : 'text-red-600'
|
||||
)}>
|
||||
{data.approval.creditAvailable ? '가능' : '불가'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{onApprove && (
|
||||
<Button
|
||||
onClick={onApprove}
|
||||
className="bg-green-500 hover:bg-green-600 gap-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
거래 승인 완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="text-center text-xs text-gray-400 pt-2 border-t">
|
||||
<p>SAM Intelligence</p>
|
||||
<p>본 신용정보 보고서에 수록된 정보는 특정기업의 신용을 평가하는 자료로 활용될 수 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CreditLevel } from './types';
|
||||
import { CREDIT_LEVEL_CONFIG } from './types';
|
||||
|
||||
interface CreditSignalProps {
|
||||
level: CreditLevel;
|
||||
taxStatus?: string;
|
||||
showDescription?: boolean;
|
||||
}
|
||||
|
||||
export function CreditSignal({
|
||||
level,
|
||||
taxStatus,
|
||||
showDescription = false,
|
||||
}: CreditSignalProps) {
|
||||
const config = CREDIT_LEVEL_CONFIG[level];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{/* 신호등 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{[1, 2, 3, 4, 5].map((l) => (
|
||||
<div
|
||||
key={l}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full border-2 transition-all',
|
||||
l <= level
|
||||
? cn(config.bgColor, 'border-transparent')
|
||||
: 'bg-gray-200 border-gray-300'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 등급 표시 */}
|
||||
<div className="text-center">
|
||||
<div className={cn('text-2xl font-bold', config.color)}>
|
||||
{config.status} (Level {level})
|
||||
</div>
|
||||
{taxStatus && (
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
{taxStatus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
{showDescription && (
|
||||
<p className="text-sm text-gray-600 text-center max-w-xs">
|
||||
{config.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Radar,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { RiskProfile, CreditDetailInfo } from './types';
|
||||
|
||||
interface RiskRadarChartProps {
|
||||
data: RiskProfile;
|
||||
creditDetailInfo?: CreditDetailInfo;
|
||||
}
|
||||
|
||||
const RISK_LABELS: Record<keyof RiskProfile, string> = {
|
||||
corporateRisk: '기업 리스크',
|
||||
publicRecord: '공공기록',
|
||||
stability: '안정성',
|
||||
growth: '성장성',
|
||||
overdueHistory: '연체이력',
|
||||
};
|
||||
|
||||
const DETAIL_LABELS: Record<keyof CreditDetailInfo, string> = {
|
||||
koreaCreditRating: '한국신용평가등급',
|
||||
financialRisk: '금융 종합 위험도',
|
||||
purchasePayment: '매입 결제',
|
||||
salesPayment: '매출 결제',
|
||||
mortgageSetting: '저당권설정',
|
||||
};
|
||||
|
||||
export function RiskRadarChart({ data, creditDetailInfo }: RiskRadarChartProps) {
|
||||
const chartData = Object.entries(data).map(([key, value]) => ({
|
||||
subject: RISK_LABELS[key as keyof RiskProfile],
|
||||
value,
|
||||
fullMark: 100,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">신용 리스크 프로필</h3>
|
||||
|
||||
<div className="w-full max-w-[300px] h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadarChart cx="50%" cy="50%" outerRadius="70%" data={chartData}>
|
||||
<PolarGrid stroke="#e5e7eb" />
|
||||
<PolarAngleAxis
|
||||
dataKey="subject"
|
||||
tick={{ fill: '#6b7280', fontSize: 11 }}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
angle={90}
|
||||
domain={[0, 100]}
|
||||
tick={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Radar
|
||||
name="신용 점수"
|
||||
dataKey="value"
|
||||
stroke="#3b82f6"
|
||||
fill="#93c5fd"
|
||||
fillOpacity={0.5}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* 레이더 차트 하단 상세 정보 */}
|
||||
{creditDetailInfo && (
|
||||
<div className="w-full mt-4 grid grid-cols-2 gap-x-6 gap-y-2">
|
||||
{Object.entries(creditDetailInfo).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">{DETAIL_LABELS[key as keyof CreditDetailInfo]}</span>
|
||||
<span className={cn(
|
||||
'font-medium',
|
||||
value === '우량' ? 'text-blue-600' :
|
||||
value === '양호' ? 'text-green-600' :
|
||||
value === '보통' ? 'text-yellow-600' :
|
||||
value === '주의' ? 'text-orange-600' :
|
||||
'text-red-600'
|
||||
)}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { CreditAnalysisDocument } from './CreditAnalysisDocument';
|
||||
import type { CreditAnalysisData } from './types';
|
||||
|
||||
interface CreditAnalysisModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
data: CreditAnalysisData;
|
||||
onApprove?: () => void;
|
||||
}
|
||||
|
||||
export function CreditAnalysisModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
onApprove,
|
||||
}: CreditAnalysisModalProps) {
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="SAM 기업 신용분석 리포트"
|
||||
subtitle={`${data.companyName} | ${data.businessNumber}`}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
features={{
|
||||
print: true,
|
||||
zoom: true,
|
||||
drag: true,
|
||||
}}
|
||||
>
|
||||
<CreditAnalysisDocument data={data} onApprove={onApprove} />
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
|
||||
export { MOCK_CREDIT_DATA } from './types';
|
||||
export type { CreditAnalysisData } from './types';
|
||||
@@ -0,0 +1,142 @@
|
||||
// ===== 기업 신용분석 데이터 타입 =====
|
||||
|
||||
export type CreditLevel = 1 | 2 | 3 | 4 | 5;
|
||||
export type CreditStatus = '위험' | '주의' | '보통' | '양호' | '우량';
|
||||
export type SafetyLevel = '안전' | '주의' | '위험';
|
||||
export type RiskStatus = '우량' | '양호' | '보통' | '주의' | '위험';
|
||||
|
||||
// 레이더 차트용 - 신용 리스크 프로필 (5각형)
|
||||
export interface RiskProfile {
|
||||
corporateRisk: number; // 기업 리스크 (0~100)
|
||||
publicRecord: number; // 공공기록 (0~100)
|
||||
stability: number; // 안정성 (0~100)
|
||||
growth: number; // 성장성 (0~100)
|
||||
overdueHistory: number; // 연체이력 (0~100)
|
||||
}
|
||||
|
||||
// 신호등 아래 표시될 상세 정보
|
||||
export interface CreditDetailInfo {
|
||||
koreaCreditRating: RiskStatus; // 한국신용평가등급
|
||||
financialRisk: RiskStatus; // 금융 종합 위험도
|
||||
purchasePayment: RiskStatus; // 매입 결제
|
||||
salesPayment: RiskStatus; // 매출 결제
|
||||
mortgageSetting: RiskStatus; // 저당권설정
|
||||
}
|
||||
|
||||
// 신용 상세 정보 탭 데이터
|
||||
export interface CreditDetailTabData {
|
||||
shortTermOverdue: string | null; // 단기연체정보
|
||||
creditJudgment: string | null; // 신용도판단정보
|
||||
checkingSuspension: string | null; // 당좌거래정지
|
||||
courtManagement: string | null; // 법정관리/워크아웃
|
||||
}
|
||||
|
||||
export interface ApprovalInfo {
|
||||
safety: SafetyLevel;
|
||||
level: CreditLevel;
|
||||
businessType: string; // 계속사업자, 신규거래 등
|
||||
creditAvailable: boolean; // 외상 가능 여부
|
||||
}
|
||||
|
||||
export interface CreditAnalysisData {
|
||||
// 기업 정보
|
||||
businessNumber: string; // 사업자번호
|
||||
companyName: string; // 법인명
|
||||
representativeName: string; // 대표자명
|
||||
evaluationDate: string; // 평가기준일
|
||||
queryDate: string; // 조회일자
|
||||
|
||||
// 신용 등급
|
||||
creditLevel: CreditLevel;
|
||||
creditStatus: CreditStatus;
|
||||
taxStatus: string; // 국세청 상태 (예: "국세청 상태 기준")
|
||||
|
||||
// 리스크 프로필 (레이더 차트용)
|
||||
riskProfile: RiskProfile;
|
||||
|
||||
// 신호등 아래 상세 정보
|
||||
creditDetailInfo: CreditDetailInfo;
|
||||
|
||||
// 신용 상세 정보 탭 데이터
|
||||
creditDetailTab: CreditDetailTabData;
|
||||
|
||||
// 거래 승인 판정
|
||||
approval: ApprovalInfo;
|
||||
}
|
||||
|
||||
// ===== 신용 레벨 설정 =====
|
||||
export const CREDIT_LEVEL_CONFIG: Record<CreditLevel, {
|
||||
status: CreditStatus;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
description: string;
|
||||
}> = {
|
||||
1: {
|
||||
status: '위험',
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-500',
|
||||
description: '신용 위험 등급으로 거래 시 주의가 필요합니다.',
|
||||
},
|
||||
2: {
|
||||
status: '주의',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-500',
|
||||
description: '신용 주의 등급으로 거래 조건 검토가 필요합니다.',
|
||||
},
|
||||
3: {
|
||||
status: '보통',
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-500',
|
||||
description: '신용 보통 등급으로 일반적인 거래가 가능합니다.',
|
||||
},
|
||||
4: {
|
||||
status: '양호',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-500',
|
||||
description: '신용 양호 등급으로 안정적인 거래가 가능합니다.',
|
||||
},
|
||||
5: {
|
||||
status: '우량',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-500',
|
||||
description: '신용 우량 등급으로 최상의 거래 조건이 가능합니다.',
|
||||
},
|
||||
};
|
||||
|
||||
// ===== 목업 데이터 =====
|
||||
export const MOCK_CREDIT_DATA: CreditAnalysisData = {
|
||||
businessNumber: '514-87-00635',
|
||||
companyName: '(주)한가 양산공장',
|
||||
representativeName: '홍길동',
|
||||
evaluationDate: '2026-01-22 21:05:04',
|
||||
queryDate: '2026-01-23 09:30:00',
|
||||
creditLevel: 4,
|
||||
creditStatus: '양호',
|
||||
taxStatus: '국세청 상태 기준',
|
||||
riskProfile: {
|
||||
corporateRisk: 80,
|
||||
publicRecord: 75,
|
||||
stability: 85,
|
||||
growth: 70,
|
||||
overdueHistory: 90,
|
||||
},
|
||||
creditDetailInfo: {
|
||||
koreaCreditRating: '양호',
|
||||
financialRisk: '양호',
|
||||
purchasePayment: '양호',
|
||||
salesPayment: '양호',
|
||||
mortgageSetting: '양호',
|
||||
},
|
||||
creditDetailTab: {
|
||||
shortTermOverdue: null,
|
||||
creditJudgment: null,
|
||||
checkingSuspension: null,
|
||||
courtManagement: null,
|
||||
},
|
||||
approval: {
|
||||
safety: '안전',
|
||||
level: 4,
|
||||
businessType: '계속사업자',
|
||||
creditAvailable: true,
|
||||
},
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { toast } from 'sonner';
|
||||
import { getClientById, createClient, updateClient, deleteClient } from './actions';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { vendorConfig } from './vendorConfig';
|
||||
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
@@ -145,6 +146,9 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
|
||||
// 새 메모 입력
|
||||
const [newMemo, setNewMemo] = useState('');
|
||||
|
||||
// 신용분석 모달
|
||||
const [isCreditModalOpen, setIsCreditModalOpen] = useState(false);
|
||||
|
||||
// Validation 함수
|
||||
const validateForm = useCallback(() => {
|
||||
const errors: Record<string, string> = {};
|
||||
@@ -478,8 +482,17 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
|
||||
|
||||
{/* 신용/거래 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg">신용/거래 정보</CardTitle>
|
||||
{isViewMode && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsCreditModalOpen(true)}
|
||||
>
|
||||
신용정보 보기
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)}
|
||||
@@ -635,6 +648,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={templateMode}
|
||||
@@ -646,5 +660,13 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
|
||||
{/* 신용분석 모달 */}
|
||||
<CreditAnalysisModal
|
||||
open={isCreditModalOpen}
|
||||
onOpenChange={setIsCreditModalOpen}
|
||||
data={MOCK_CREDIT_DATA}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { vendorConfig } from './vendorConfig';
|
||||
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
|
||||
import type { Vendor, VendorMemo } from './types';
|
||||
import {
|
||||
VENDOR_CATEGORY_SELECTOR_OPTIONS,
|
||||
@@ -113,6 +114,9 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
// 새 메모 입력
|
||||
const [newMemo, setNewMemo] = useState('');
|
||||
|
||||
// 신용분석 모달
|
||||
const [isCreditModalOpen, setIsCreditModalOpen] = useState(false);
|
||||
|
||||
// 상세/수정 모드에서 로고 목데이터 초기화
|
||||
useEffect(() => {
|
||||
if (initialData && !formData.logoUrl) {
|
||||
@@ -428,8 +432,15 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
|
||||
{/* 신용/거래 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg">신용/거래 정보</CardTitle>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsCreditModalOpen(true)}
|
||||
>
|
||||
신용정보 보기
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)}
|
||||
@@ -549,6 +560,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={templateMode}
|
||||
@@ -559,5 +571,13 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
|
||||
{/* 신용분석 모달 */}
|
||||
<CreditAnalysisModal
|
||||
open={isCreditModalOpen}
|
||||
onOpenChange={setIsCreditModalOpen}
|
||||
data={MOCK_CREDIT_DATA}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMenuStore } from '@/store/menuStore';
|
||||
import type { SerializableMenuItem } from '@/store/menuStore';
|
||||
import type { MenuItem } from '@/store/menuStore';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Menu,
|
||||
@@ -572,8 +573,8 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
|
||||
onClick={handleGoHome}
|
||||
title="대시보드로 이동"
|
||||
>
|
||||
<div className="w-6 h-6 min-w-6 min-h-6 min-[320px]:w-8 min-[320px]:h-8 min-[320px]:min-w-8 min-[320px]:min-h-8 sm:w-10 sm:h-10 sm:min-w-10 sm:min-h-10 flex-shrink-0 aspect-square rounded-lg min-[320px]:rounded-xl flex items-center justify-center shadow-md relative overflow-hidden bg-gradient-to-br from-blue-500 to-blue-600">
|
||||
<div className="text-white font-bold text-sm min-[320px]:text-base sm:text-lg">S</div>
|
||||
<div className="w-6 h-6 min-w-6 min-h-6 min-[320px]:w-8 min-[320px]:h-8 min-[320px]:min-w-8 min-[320px]:min-h-8 sm:w-10 sm:h-10 sm:min-w-10 sm:min-h-10 flex-shrink-0 aspect-square rounded-lg min-[320px]:rounded-xl flex items-center justify-center shadow-md relative overflow-hidden">
|
||||
<Image src="/sam-logo.png" alt="SAM" fill className="object-contain p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-foreground text-left text-xs min-[320px]:text-sm sm:text-base">SAM</h1>
|
||||
@@ -830,8 +831,8 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
|
||||
onClick={handleGoHome}
|
||||
title="대시보드로 이동"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center shadow-md relative overflow-hidden bg-gradient-to-br from-blue-500 to-blue-600 flex-shrink-0">
|
||||
<div className="text-white font-bold text-xl">S</div>
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center shadow-md relative overflow-hidden flex-shrink-0">
|
||||
<Image src="/sam-logo.png" alt="SAM" fill className="object-contain p-1" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">SAM</h1>
|
||||
|
||||
Reference in New Issue
Block a user