feat(WEB): 파비콘 변경 및 거래처 신용분석 모달 추가

- 파비콘: SVG 형식으로 변경 (white 로고 + 파란 배경)
- 헤더 로고: SAM 로고 이미지로 교체
- 거래처 상세: 신용분석 모달 컴포넌트 추가
  - 신용등급, 리스크 지표, 레이더 차트
  - 프린트 기능 지원

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-23 11:14:48 +09:00
parent af5fdcba88
commit ad063a1f01
12 changed files with 716 additions and 27 deletions

View 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

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
View 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,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"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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';

View File

@@ -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,
},
};

View File

@@ -8,6 +8,7 @@ import { toast } from 'sonner';
import { getClientById, createClient, updateClient, deleteClient } from './actions'; import { getClientById, createClient, updateClient, deleteClient } from './actions';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { vendorConfig } from './vendorConfig'; import { vendorConfig } from './vendorConfig';
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
// 필드명 매핑 // 필드명 매핑
const FIELD_NAME_MAP: Record<string, string> = { const FIELD_NAME_MAP: Record<string, string> = {
@@ -145,6 +146,9 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
// 새 메모 입력 // 새 메모 입력
const [newMemo, setNewMemo] = useState(''); const [newMemo, setNewMemo] = useState('');
// 신용분석 모달
const [isCreditModalOpen, setIsCreditModalOpen] = useState(false);
// Validation 함수 // Validation 함수
const validateForm = useCallback(() => { const validateForm = useCallback(() => {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
@@ -478,8 +482,17 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
{/* 신용/거래 정보 */} {/* 신용/거래 정보 */}
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">/ </CardTitle> <CardTitle className="text-lg">/ </CardTitle>
{isViewMode && (
<Button
variant="outline"
size="sm"
onClick={() => setIsCreditModalOpen(true)}
>
</Button>
)}
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4"> <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)} {renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)}
@@ -635,16 +648,25 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
}; };
return ( return (
<IntegratedDetailTemplate <>
config={dynamicConfig} <IntegratedDetailTemplate
mode={templateMode} config={dynamicConfig}
initialData={formData as unknown as Record<string, unknown>} mode={templateMode}
itemId={vendorId} initialData={formData as unknown as Record<string, unknown>}
isLoading={isLoading} itemId={vendorId}
onSubmit={handleSubmit} isLoading={isLoading}
onDelete={vendorId ? handleDelete : undefined} onSubmit={handleSubmit}
renderView={() => renderFormContent()} onDelete={vendorId ? handleDelete : undefined}
renderForm={() => renderFormContent()} renderView={() => renderFormContent()}
/> renderForm={() => renderFormContent()}
/>
{/* 신용분석 모달 */}
<CreditAnalysisModal
open={isCreditModalOpen}
onOpenChange={setIsCreditModalOpen}
data={MOCK_CREDIT_DATA}
/>
</>
); );
} }

View File

@@ -20,6 +20,7 @@ import {
} from '@/components/ui/select'; } from '@/components/ui/select';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { vendorConfig } from './vendorConfig'; import { vendorConfig } from './vendorConfig';
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
import type { Vendor, VendorMemo } from './types'; import type { Vendor, VendorMemo } from './types';
import { import {
VENDOR_CATEGORY_SELECTOR_OPTIONS, VENDOR_CATEGORY_SELECTOR_OPTIONS,
@@ -113,6 +114,9 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
// 새 메모 입력 // 새 메모 입력
const [newMemo, setNewMemo] = useState(''); const [newMemo, setNewMemo] = useState('');
// 신용분석 모달
const [isCreditModalOpen, setIsCreditModalOpen] = useState(false);
// 상세/수정 모드에서 로고 목데이터 초기화 // 상세/수정 모드에서 로고 목데이터 초기화
useEffect(() => { useEffect(() => {
if (initialData && !formData.logoUrl) { if (initialData && !formData.logoUrl) {
@@ -428,8 +432,15 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
{/* 신용/거래 정보 */} {/* 신용/거래 정보 */}
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">/ </CardTitle> <CardTitle className="text-lg">/ </CardTitle>
<Button
variant="outline"
size="sm"
onClick={() => setIsCreditModalOpen(true)}
>
</Button>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4"> <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)} {renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_SELECTOR_OPTIONS)}
@@ -549,15 +560,24 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
}; };
return ( return (
<IntegratedDetailTemplate <>
config={dynamicConfig} <IntegratedDetailTemplate
mode={templateMode} config={dynamicConfig}
initialData={initialData as Record<string, unknown>} mode={templateMode}
itemId={vendorId} initialData={initialData as Record<string, unknown>}
onSubmit={handleSubmit} itemId={vendorId}
onDelete={vendorId ? handleDelete : undefined} onSubmit={handleSubmit}
renderView={() => renderFormContent()} onDelete={vendorId ? handleDelete : undefined}
renderForm={() => renderFormContent()} renderView={() => renderFormContent()}
/> renderForm={() => renderFormContent()}
/>
{/* 신용분석 모달 */}
<CreditAnalysisModal
open={isCreditModalOpen}
onOpenChange={setIsCreditModalOpen}
data={MOCK_CREDIT_DATA}
/>
</>
); );
} }

View File

@@ -4,6 +4,7 @@ import { useMenuStore } from '@/store/menuStore';
import type { SerializableMenuItem } from '@/store/menuStore'; import type { SerializableMenuItem } from '@/store/menuStore';
import type { MenuItem } from '@/store/menuStore'; import type { MenuItem } from '@/store/menuStore';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from 'next/navigation';
import Image from 'next/image';
import { useEffect, useState, useMemo, useCallback } from 'react'; import { useEffect, useState, useMemo, useCallback } from 'react';
import { import {
Menu, Menu,
@@ -572,8 +573,8 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
onClick={handleGoHome} onClick={handleGoHome}
title="대시보드로 이동" 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="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">
<div className="text-white font-bold text-sm min-[320px]:text-base sm:text-lg">S</div> <Image src="/sam-logo.png" alt="SAM" fill className="object-contain p-0.5" />
</div> </div>
<div> <div>
<h1 className="font-bold text-foreground text-left text-xs min-[320px]:text-sm sm:text-base">SAM</h1> <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} onClick={handleGoHome}
title="대시보드로 이동" 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="w-12 h-12 rounded-xl flex items-center justify-center shadow-md relative overflow-hidden flex-shrink-0">
<div className="text-white font-bold text-xl">S</div> <Image src="/sam-logo.png" alt="SAM" fill className="object-contain p-1" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-foreground">SAM</h1> <h1 className="text-xl font-bold text-foreground">SAM</h1>