From 87aff06c7ea3043d606a75b0731240d5a51ce7cf Mon Sep 17 00:00:00 2001 From: Gyeoul Date: Thu, 22 May 2025 02:24:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=ED=98=91=EB=A0=A5=EC=82=AC=20?= =?UTF-8?q?=EC=9E=AC=EB=AC=B4=20=EC=9C=84=ED=97=98=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20UI=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../financialRisk/financialRiskForm.tsx | 489 +++++++--- .../(partnerCompany)/financialRisk/page.tsx | 25 +- .../(partnerCompany)/managePartner/page.tsx | 875 ++++++++++++------ src/components/ui/table.tsx | 120 +++ src/services/partnerCompany.ts | 513 +++++++--- 5 files changed, 1459 insertions(+), 563 deletions(-) create mode 100644 src/components/ui/table.tsx diff --git a/src/app/(dashboard)/(partnerCompany)/financialRisk/financialRiskForm.tsx b/src/app/(dashboard)/(partnerCompany)/financialRisk/financialRiskForm.tsx index 826799f..f4c91a8 100644 --- a/src/app/(dashboard)/(partnerCompany)/financialRisk/financialRiskForm.tsx +++ b/src/app/(dashboard)/(partnerCompany)/financialRisk/financialRiskForm.tsx @@ -1,9 +1,28 @@ 'use client' -import {useState} from 'react' -import {Check, ChevronRight, FileEdit, FileText, Home} from 'lucide-react' -import {cn} from '@/lib/utils' +import {useState, useEffect} from 'react' +import { + ChevronRight, + Home, + Building2, + AlertTriangle, + CheckCircle, + Info, + ChevronsDown, + ChevronsUp, + FileSearch, + Check, + ChevronsUpDown +} from 'lucide-react' import {Button} from '@/components/ui/button' +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription +} from '@/components/ui/card' +import {PageHeader} from '@/components/layout/PageHeader' import { Command, CommandEmpty, @@ -13,64 +32,76 @@ import { CommandList } from '@/components/ui/command' import {Popover, PopoverContent, PopoverTrigger} from '@/components/ui/popover' -import {Card, CardContent} from '@/components/ui/card' +import {cn} from '@/lib/utils' +import {useToast} from '@/hooks/use-toast' +import {LoadingState} from '@/components/ui/loading-state' import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger -} from '@/components/ui/dialog' -import {BreadcrumbLink} from '@/components/ui/breadcrumb' -import {PageHeader} from '@/components/layout/PageHeader' + fetchUniquePartnerCompanyNames, + fetchFinancialRiskAssessment, + type FinancialRiskAssessment +} from '@/services/partnerCompany' -const partners = ['협력사 A', '협력사 B', '협력사 C', '협력사 D', '협력사 E'] - -const questions = [ - '매출이 지난 해 같은 기간 대비 30% 이상 감소하였는가?', - '영업이익(흑자)이 지난 해 같은 기간 대비 30% 이상 감소하였는가?', - '매출채권회전율이 3회 이하이며 매출채권이 과대하게 쌓였는가?', - '매출액 대비 매출채권 비율이 50% 이상으로 과다한가?', - '매출액 대비 재고자산 비율이 30% 이상이며 미래재무구가 과다한가?', - '영업손실(적자)이 발생하였는가?', - '불충족자본비율산정 영업활동 후의 현금흐름에 적자가 발생하였는가?', - '차입금이 지난 해 같은 기간 대비 30% 이상 증가하였는가?', - '차입금의 Volume이 전체 자산의 50% 이상 차지할 정도로 과다한가?', - '전체 사업의 총 단기차입금의 규모가 90%이상으로 과다한가?', - '재무비율이 200% 이상으로 과다한가?', - '납입자본금의 잠식이 발생하였는가?' +// API 응답 타입 정의 +interface RiskItem { + description: string + actualValue: string + threshold: string + notes: string | null + itemNumber: number + atRisk: boolean +} + +interface FinancialRiskData { + partnerCompanyId: string + partnerCompanyName: string + assessmentYear: string + reportCode: string + riskItems: RiskItem[] +} + +// 협력사 데이터 구조 변경 (이름과 DART 코드 포함) +// 실제 운영 환경에서는 이 목록을 API로부터 받아오거나, 다른 방식으로 관리해야 합니다. +const partners = [ + {name: '협력사 A', code: '00126380'}, + {name: '협력사 B', code: '00123456'}, + {name: '협력사 C', code: '00789012'}, + {name: '협력사 D', code: '00345678'}, + {name: '협력사 E', code: '00901234'} ] -const partnerQuestionMap: {[key: string]: number[]} = { - '협력사 A': [], - '협력사 B': [1, 5, 10], - '협력사 C': [0, 2, 3, 4, 6, 8, 9, 11], - '협력사 D': [1], - '협력사 E': [2, 4, 5, 8] +// 상태 레이블 유틸리티 함수 +function getStatusLabel(atRiskCount: number) { + if (atRiskCount === 0) { + return { + label: '안전', + color: 'text-emerald-600', + icon: + } + } + if (atRiskCount <= 2) { + return { + label: '주의', + color: 'text-amber-600', + icon: + } + } + return { + label: '위험', + color: 'text-red-600', + icon: + } } -function getStatusLabel(count: number) { - if (count === 0) - return {text: '✅ 기업 재무 상태 부실화 양호 ✅', color: 'text-green-600'} - if (count === 1) - return {text: '🔵 기업 재무 상태 부실화 관심 🔵', color: 'text-blue-600'} - if (count >= 2 && count <= 3) - return {text: '⚠️ 기업 재무 상태 부실화 주의 ⚠️', color: 'text-yellow-600'} - if (count >= 4 && count <= 5) - return {text: '‼️ 기업 재무 상태 부실화 경계 ‼️', color: 'text-orange-600'} - return {text: '🚨 기업 재무 상태 부실화 심각 🚨', color: 'text-red-600'} +// PartnerCombobox의 props 타입 수정 +interface PartnerComboboxProps { + options: Array<{name: string; code: string}> + value: string | null // 선택된 협력사의 DART 코드 + onChange: (code: string) => void } -function PartnerCombobox({ - options, - value, - onChange -}: { - options: string[] - value: string | null - onChange: (value: string) => void -}) { +function PartnerCombobox({options, value, onChange}: PartnerComboboxProps) { const [open, setOpen] = useState(false) + const selectedOption = options.find(option => option.code === value) return ( @@ -79,20 +110,24 @@ function PartnerCombobox({ variant="outline" role="combobox" aria-expanded={open} - className="w-[200px] justify-between"> - {value || '협력사 선택'} + className="w-full justify-between" // 너비를 full로 변경하거나 적절한 값으로 조절 + > + {selectedOption ? selectedOption.name : '협력사 선택...'} + - + - 검색 결과가 없습니다. + 해당하는 협력사가 없습니다. - {options.map(partner => ( + {options.map(option => ( { onChange(currentValue === value ? '' : currentValue) setOpen(false) @@ -100,10 +135,10 @@ function PartnerCombobox({ - {partner} + {option.name} ))} @@ -115,7 +150,116 @@ function PartnerCombobox({ } export default function FinancialRiskForm() { - const [selectedPartner, setSelectedPartner] = useState(null) + const {toast} = useToast() + + // 상태 관리 + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [partnerOptions, setPartnerOptions] = useState< + Array<{name: string; code: string}> + >([]) + const [selectedPartnerCode, setSelectedPartnerCode] = useState(null) + const [selectedPartnerName, setSelectedPartnerName] = useState(null) + const [riskData, setRiskData] = useState(null) + const [expandedItems, setExpandedItems] = useState>(new Set()) + + // 확장/축소 토글 함수 + const toggleExpand = (itemNumber: number) => { + setExpandedItems(prev => { + const newSet = new Set(prev) + if (newSet.has(itemNumber)) { + newSet.delete(itemNumber) + } else { + newSet.add(itemNumber) + } + return newSet + }) + } + + // 모든 항목 확장/축소 함수 + const toggleAllExpanded = (expand: boolean) => { + if (riskData?.riskItems) { + if (expand) { + const allNumbers = new Set(riskData.riskItems.map(item => item.itemNumber)) + setExpandedItems(allNumbers) + } else { + setExpandedItems(new Set()) + } + } + } + + // 초기 데이터 로드 + useEffect(() => { + loadPartnerOptions() + }, []) + + // 파트너사 옵션 로드 + const loadPartnerOptions = async () => { + try { + setIsLoading(true) + + // 먼저 고유 파트너사명 목록을 가져옵니다. + const partnerNames = await fetchUniquePartnerCompanyNames() + console.log('Loaded partner names:', partnerNames) + + // 목록은 문자열 배열이므로 옵션 형식으로 변환합니다. + // API가 아직 코드를 제공하지 않는 경우 임시 처리 + const options = partnerNames.map(name => ({ + name, + code: name // 임시로 이름을 코드로 사용 + })) + + setPartnerOptions(options) + } catch (err) { + console.error('Failed to load partner options:', err) + setError('파트너사 목록을 불러오는데 실패했습니다.') + toast({ + variant: 'destructive', + title: '오류', + description: '파트너사 목록을 불러오는데 실패했습니다.' + }) + } finally { + setIsLoading(false) + } + } + + // 파트너사 선택 시 핸들러 + const handlePartnerSelect = async (code: string) => { + setSelectedPartnerCode(code) + + // 선택된 파트너사의 이름 찾기 + const selectedOption = partnerOptions.find(opt => opt.code === code) + if (selectedOption) { + setSelectedPartnerName(selectedOption.name) + } + + try { + setIsLoading(true) + setError(null) + setRiskData(null) + + // 재무 위험 분석 데이터 가져오기 + const data = await fetchFinancialRiskAssessment(code, selectedOption?.name) + setRiskData(data) + + // 항목 확장 상태 초기화 + setExpandedItems(new Set()) + } catch (err) { + console.error('Failed to load financial risk data:', err) + setError('재무 위험 데이터를 불러오는데 실패했습니다.') + toast({ + variant: 'destructive', + title: '오류', + description: '재무 위험 데이터를 불러오는데 실패했습니다.' + }) + } finally { + setIsLoading(false) + } + } + + // 위험 항목 수 계산 + const atRiskCount = riskData?.riskItems?.filter(item => item.atRisk).length || 0 + const statusInfo = getStatusLabel(atRiskCount) return (
@@ -129,94 +273,147 @@ export default function FinancialRiskForm() {
} - title="재무제표 리스크 관리" - module="GRI"> - - {selectedPartner && ( -

- {selectedPartner}는 항목 총 {partnerQuestionMap[selectedPartner].length}개 :{' '} - {getStatusLabel(partnerQuestionMap[selectedPartner].length).text} -

- )} - -
- + icon={} + title="파트너사 재무 위험 분석" + description="파트너사의 재무 건전성과 위험을 분석합니다." + module="CSDD" + /> + +
+
+ +
+
- {selectedPartner ? ( - - - - - - - - - - - {questions.map((q, i) => ( - - - - - ))} - -
항목 - 해당 여부 -
{`${ - i + 1 - }. ${q}`} - {partnerQuestionMap[selectedPartner]?.includes(i) ? ( - - ) : null} -
-
-
- ) : ( -
-
-
- + + {riskData && ( +
+
+ + + 파트너사 + + +
{riskData.partnerCompanyName}
+

+ 사업자번호: {riskData.partnerCompanyId} +

+
+
+ + + + 기준 정보 + + +
{riskData.assessmentYear}년
+

+ 보고서 코드: {riskData.reportCode} +

+
+
+ + + + 재무 위험 상태 + + +
+
{statusInfo.label}
+ {statusInfo.icon} +
+

+ 위험 항목: {atRiskCount} / {riskData.riskItems.length} 항목 +

+
+
-

- 데이터가 없습니다 -

-

- 선택된 협력사가 없습니다. 협력사를 선택해보세요. -

-
-
- )} - - {selectedPartner && ( -
- - - - - - - 재무제표 상세 - -