288 lines
8.8 KiB
TypeScript
288 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { formatNumber } from '@/lib/utils/amount';
|
|
import type { IncomeStatementMonthlyData, IncomeStatementSection } from './types';
|
|
import { HIGHLIGHT_CODES } from './types';
|
|
|
|
interface MonthlyViewProps {
|
|
data: IncomeStatementMonthlyData;
|
|
selectedMonth: string | null; // null = 전체
|
|
unitLabel: string;
|
|
}
|
|
|
|
export function MonthlyView({ data, selectedMonth, unitLabel }: MonthlyViewProps) {
|
|
const { months, fiscalLabel } = data;
|
|
|
|
if (selectedMonth) {
|
|
// 개별 월 보기
|
|
const monthData = months.find((m) => m.month === selectedMonth);
|
|
if (!monthData) {
|
|
return <div className="text-center py-8 text-muted-foreground text-sm">해당 월 데이터가 없습니다.</div>;
|
|
}
|
|
return (
|
|
<SingleMonthView
|
|
sections={monthData.sections}
|
|
monthLabel={monthData.label}
|
|
fiscalLabel={fiscalLabel}
|
|
unitLabel={unitLabel}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 전체 월 비교 보기 (가로 스크롤)
|
|
return (
|
|
<AllMonthsView
|
|
months={months}
|
|
fiscalLabel={fiscalLabel}
|
|
unitLabel={unitLabel}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 개별 월 보기 — PeriodView와 동일한 2열(금액+소계) 구조
|
|
function SingleMonthView({
|
|
sections,
|
|
monthLabel,
|
|
fiscalLabel,
|
|
unitLabel,
|
|
}: {
|
|
sections: IncomeStatementSection[];
|
|
monthLabel: string;
|
|
fiscalLabel: string;
|
|
unitLabel: string;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="rounded-md border overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-green-700 hover:bg-green-700">
|
|
<TableHead
|
|
rowSpan={2}
|
|
className="font-semibold text-xs md:text-sm text-white w-[40%] border-r border-green-600"
|
|
>
|
|
과 목
|
|
</TableHead>
|
|
<TableHead
|
|
colSpan={2}
|
|
className="font-semibold text-center text-xs md:text-sm text-white border-b border-green-600"
|
|
>
|
|
{fiscalLabel} {monthLabel}
|
|
</TableHead>
|
|
</TableRow>
|
|
<TableRow className="bg-green-700 hover:bg-green-700">
|
|
<TableHead
|
|
colSpan={2}
|
|
className="font-semibold text-center text-xs md:text-sm text-white"
|
|
>
|
|
금 액
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{sections.map((section) => {
|
|
const isHighlight = HIGHLIGHT_CODES.includes(section.code);
|
|
const highlightClass = isHighlight ? 'bg-green-50' : '';
|
|
|
|
// 계산 항목 또는 세부과목 없는 항목
|
|
if (section.items.length === 0) {
|
|
return (
|
|
<TableRow key={section.code} className={highlightClass}>
|
|
<TableCell className="text-xs md:text-sm font-semibold">
|
|
{section.code}. {section.name}
|
|
</TableCell>
|
|
<TableCell />
|
|
<TableCell className={`text-right text-xs md:text-sm whitespace-nowrap ${isHighlight ? 'font-bold' : 'font-semibold'}`}>
|
|
{section.currentAmount !== 0 ? formatNumber(section.currentAmount) : ''}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
// 세부과목 있는 항목
|
|
const lastIdx = section.items.length - 1;
|
|
return (
|
|
<SingleMonthSectionRows
|
|
key={section.code}
|
|
section={section}
|
|
lastIdx={lastIdx}
|
|
/>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground text-right px-1">
|
|
(단위: {unitLabel})
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SingleMonthSectionRows({
|
|
section,
|
|
lastIdx,
|
|
}: {
|
|
section: IncomeStatementSection;
|
|
lastIdx: number;
|
|
}) {
|
|
return (
|
|
<>
|
|
<TableRow>
|
|
<TableCell className="text-xs md:text-sm font-semibold">
|
|
{section.code}. {section.name}
|
|
</TableCell>
|
|
<TableCell />
|
|
<TableCell />
|
|
</TableRow>
|
|
{section.items.map((item, idx) => {
|
|
const isLast = idx === lastIdx;
|
|
return (
|
|
<TableRow key={item.code}>
|
|
<TableCell className="text-xs md:text-sm pl-8 text-muted-foreground">
|
|
{item.name}
|
|
</TableCell>
|
|
<TableCell className="text-right text-xs md:text-sm whitespace-nowrap">
|
|
{item.current !== 0 ? formatNumber(item.current) : ''}
|
|
</TableCell>
|
|
<TableCell className="text-right text-xs md:text-sm whitespace-nowrap font-semibold">
|
|
{isLast ? formatNumber(section.currentAmount) : ''}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// 전체 월 비교 보기 (가로 스크롤, 과목 열 sticky)
|
|
function AllMonthsView({
|
|
months,
|
|
fiscalLabel,
|
|
unitLabel,
|
|
}: {
|
|
months: IncomeStatementMonthlyData['months'];
|
|
fiscalLabel: string;
|
|
unitLabel: string;
|
|
}) {
|
|
const baseSections = months[0]?.sections || [];
|
|
|
|
const rows: {
|
|
type: 'section' | 'item';
|
|
code: string;
|
|
name: string;
|
|
sectionCode: string;
|
|
isHighlight: boolean;
|
|
}[] = [];
|
|
|
|
baseSections.forEach((section) => {
|
|
const isHighlight = HIGHLIGHT_CODES.includes(section.code);
|
|
rows.push({
|
|
type: 'section',
|
|
code: section.code,
|
|
name: `${section.code}. ${section.name}`,
|
|
sectionCode: section.code,
|
|
isHighlight,
|
|
});
|
|
section.items.forEach((item) => {
|
|
rows.push({
|
|
type: 'item',
|
|
code: item.code,
|
|
name: item.name,
|
|
sectionCode: section.code,
|
|
isHighlight: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
const monthMaps = months.map((m) => {
|
|
const sectionMap = new Map<string, number>();
|
|
const itemMap = new Map<string, number>();
|
|
m.sections.forEach((s) => {
|
|
sectionMap.set(s.code, s.currentAmount);
|
|
s.items.forEach((it) => {
|
|
itemMap.set(`${s.code}-${it.code}`, it.current);
|
|
});
|
|
});
|
|
return { sectionMap, itemMap };
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="rounded-md border overflow-x-auto">
|
|
<div style={{ minWidth: `${200 + months.length * 120}px` }}>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead
|
|
className="font-semibold text-xs md:text-sm sticky left-0 z-20 bg-background min-w-[200px]"
|
|
style={{ boxShadow: '2px 0 4px -2px rgba(0,0,0,0.1)' }}
|
|
>
|
|
과목
|
|
</TableHead>
|
|
{months.map((m) => (
|
|
<TableHead
|
|
key={m.month}
|
|
className="font-semibold text-right text-xs md:text-sm min-w-[110px] whitespace-nowrap"
|
|
>
|
|
{m.label}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((row) => {
|
|
const isSection = row.type === 'section';
|
|
const highlightClass = row.isHighlight ? 'bg-green-50' : '';
|
|
const sectionBg = isSection && !row.isHighlight ? 'bg-muted/30' : '';
|
|
|
|
return (
|
|
<TableRow
|
|
key={`${row.sectionCode}-${row.code}`}
|
|
className={`${highlightClass} ${sectionBg}`.trim() || undefined}
|
|
>
|
|
<TableCell
|
|
className={`text-xs md:text-sm sticky left-0 z-10 bg-inherit ${
|
|
isSection ? 'font-semibold' : 'pl-8 text-muted-foreground'
|
|
}`}
|
|
style={{ boxShadow: '2px 0 4px -2px rgba(0,0,0,0.1)' }}
|
|
>
|
|
{row.name}
|
|
</TableCell>
|
|
{months.map((m, mi) => {
|
|
const amount = isSection
|
|
? monthMaps[mi].sectionMap.get(row.sectionCode) ?? 0
|
|
: monthMaps[mi].itemMap.get(`${row.sectionCode}-${row.code}`) ?? 0;
|
|
return (
|
|
<TableCell
|
|
key={m.month}
|
|
className={`text-right text-xs md:text-sm whitespace-nowrap ${
|
|
isSection ? 'font-semibold' : ''
|
|
}`}
|
|
>
|
|
{formatNumber(amount)}
|
|
</TableCell>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground text-right px-1">
|
|
(단위: {unitLabel})
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|