1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
"use client";
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface SalesInfoTableProps {
creditLoading: boolean;
creditError: string | null;
creditResults: any[];
selectedCreditService: string;
bestResult: any;
getCurrentResult: () => any | null;
handleCreditServiceChange: (code: string) => void;
creditServices: { code: string; name: string }[];
}
export function SalesInfoTable({
creditLoading,
creditError,
creditResults,
selectedCreditService,
bestResult,
getCurrentResult,
handleCreditServiceChange,
creditServices,
}: SalesInfoTableProps) {
// 'auto' 선택 시 bestResult 우선 사용, 수동 선택 시 해당 코드의 결과 사용
const currentResult = React.useMemo(() => {
// 'auto' 선택 시에만 bestResult 사용
if (selectedCreditService === "auto") {
return bestResult || getCurrentResult?.() || null;
}
// 수동 선택 시 해당 평가사 결과
return creditResults.find((r) => r.code === selectedCreditService) || null;
}, [selectedCreditService, bestResult, creditResults, getCurrentResult]);
const data = currentResult?.data || null;
// 가용한 지표 인덱스 탐색: bs_dt{n} 키를 모두 수집해 인덱스 배열 생성
const availableIndexes: number[] = React.useMemo(() => {
if (!data) return [];
// 0,1,2(3개년)만 표시. 해당 인덱스의 기준일 키가 존재하는 경우만 포함
return [0, 1, 2].filter((n) => (data as any)[`BS_DT${n}`] || (data as any)[`bs_dt${n}`]);
}, [data]);
// TR 계열(비율) 키는 연도 인덱스에 따라 0→3, 1→2, 2→1 매핑됨
const mapIdxToSuffix = (idx: number): string | null => {
if (idx === 0) return "3";
if (idx === 1) return "2";
if (idx === 2) return "1";
return null; // 3년 초과 시 해당 비율 데이터가 없을 수 있음
};
const getField = (prefix: string, idx: number): string => {
// prefix는 소문자 기준(e.g., bs59_, pl27_), 대문자 키(e.g., BS59_)도 함께 조회
const lowerKey = `${prefix}${idx}`;
const upperKey = `${prefix.toUpperCase()}${idx}`;
const v = (data && (data[lowerKey] ?? data[upperKey])) as unknown as string | number | undefined | null;
return v !== undefined && v !== null && v !== "" ? String(v) : "-";
};
const getRatio = (baseKey: string, idx: number): string => {
const suffix = mapIdxToSuffix(idx);
if (!suffix) return "-";
const key = `${baseKey}${suffix}`;
const v = data?.[key];
return v && v !== "" ? String(v) : "-";
};
return (
<div className="space-y-4">
<div className="p-4 border-b bg-muted/30">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">신용평가사 데이터</span>
{creditLoading && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
로딩 중...
</div>
)}
</div>
<div className="flex items-center gap-4">
<Select value={selectedCreditService} onValueChange={handleCreditServiceChange}>
<SelectTrigger className="w-48">
<SelectValue placeholder="신용평가사 선택" />
</SelectTrigger>
<SelectContent>
{creditServices.map((service) => {
const result = creditResults.find((r) => r.code === service.code);
return (
<SelectItem key={service.code} value={service.code}>
{service.name}
{service.code !== "auto" && result && (
<span className="ml-2 text-xs text-muted-foreground">
{result.success ? `(${result.dataCount}개 항목)` : "(조회 실패)"}
</span>
)}
</SelectItem>
);
})}
</SelectContent>
</Select>
{currentResult && (
<div className="text-sm text-muted-foreground">
선택됨: <span className="font-medium">{currentResult?.name}</span>
{selectedCreditService === "auto" && bestResult && (
<span className="ml-1">({bestResult.dataCount}개 항목으로 자동선택)</span>
)}
</div>
)}
{creditError && <div className="text-sm text-destructive">{creditError}</div>}
{!creditLoading && !creditError && creditResults.length > 0 && !currentResult?.data && (
<div className="text-sm text-muted-foreground">신용평가 데이터가 없습니다</div>
)}
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead rowSpan={2} className="text-center border-r align-middle">기준일</TableHead>
<TableHead colSpan={3} className="text-center border-r">자산 구성</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">매출액<br />(백만원)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">영업이익<br />(백만원)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">당기순이익<br />(백만원)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">부채비율<br />(%)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">차입금의존도<br />(%)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">영업이익률<br />(%)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">순이익률<br />(%)</TableHead>
<TableHead rowSpan={2} className="text-center border-r align-middle">매출액증감<br />(%)</TableHead>
<TableHead rowSpan={2} className="text-center align-middle">유동비율<br />(%)</TableHead>
</TableRow>
<TableRow>
<TableHead className="text-center border-r">총자산</TableHead>
<TableHead className="text-center border-r">부채총계</TableHead>
<TableHead className="text-center border-r">자본총계</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{availableIndexes.length === 0 && (
<TableRow>
<TableCell colSpan={13} className="text-center text-sm text-muted-foreground">표시할 데이터가 없습니다</TableCell>
</TableRow>
)}
{availableIndexes.map((idx) => (
<TableRow key={`row-${idx}`}>
<TableCell className="text-center font-medium border-r bg-yellow-50">{(data?.[`BS_DT${idx}`] || data?.[`bs_dt${idx}`] || "-") as any}</TableCell>
<TableCell className="text-right border-r">{getField("bs59_", idx)}</TableCell>
<TableCell className="text-right border-r">{getField("bs91_", idx)}</TableCell>
<TableCell className="text-right border-r">{getField("bs113_", idx)}</TableCell>
<TableCell className="text-right border-r">{getField("pl01_", idx)}</TableCell>
<TableCell className="text-right border-r">{getField("pl27_", idx)}</TableCell>
<TableCell className="text-right border-r">{getField("pl71_", idx)}</TableCell>
<TableCell className="text-right border-r">{getRatio("TR005", idx)}</TableCell>
<TableCell className="text-right border-r">{getRatio("TR051", idx)}</TableCell>
<TableCell className="text-right border-r">{getRatio("TR052", idx)}</TableCell>
<TableCell className="text-right border-r">{getRatio("TR010", idx)}</TableCell>
<TableCell className="text-right border-r">{getRatio("TR022", idx)}</TableCell>
<TableCell className="text-right">{getRatio("TR001", idx)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
|