- 미사용 import/변수/console.log 대량 정리 (100+개 파일) - ItemMasterContext 간소화 (미사용 로직 제거) - IntegratedListTemplateV2 / UniversalListPage 개선 - 결재 컴포넌트(ApprovalBox, DraftBox, ReferenceBox) 정리 - HR 컴포넌트(급여/휴가/부서) 코드 간소화 - globals.css 스타일 정리 및 개선 - AuthenticatedLayout 개선 - middleware CSP 정리 - proxy route 불필요 로깅 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
197 lines
7.0 KiB
TypeScript
197 lines
7.0 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { QuantityInput } from '@/components/ui/quantity-input';
|
|
import { CurrencyInput } from '@/components/ui/currency-input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import type { ExpenseItem } from '../types';
|
|
import { formatAmount } from '../utils';
|
|
|
|
// 공과 옵션 타입
|
|
interface ExpenseOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
interface ExpenseDetailSectionProps {
|
|
expenseItems: ExpenseItem[];
|
|
expenseOptions: ExpenseOption[]; // 공과 품목 옵션 (Items API에서 조회)
|
|
isViewMode: boolean;
|
|
onAddItems: (count: number) => void;
|
|
onRemoveSelected: () => void;
|
|
onItemChange: (id: string, field: keyof ExpenseItem, value: string | number) => void;
|
|
onSelectItem: (id: string, selected: boolean) => void;
|
|
onSelectAll: (selected: boolean) => void;
|
|
}
|
|
|
|
export function ExpenseDetailSection({
|
|
expenseItems,
|
|
expenseOptions,
|
|
isViewMode,
|
|
onAddItems,
|
|
onRemoveSelected,
|
|
onItemChange,
|
|
onSelectItem,
|
|
onSelectAll,
|
|
}: ExpenseDetailSectionProps) {
|
|
const [expenseAddCount, setExpenseAddCount] = useState<number | undefined>(1);
|
|
const selectedCount = expenseItems.filter((item) => item.selected).length;
|
|
const allSelected = expenseItems.length > 0 && expenseItems.every((item) => item.selected);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
|
<div className="flex items-center gap-4">
|
|
<CardTitle className="text-lg">공과 상세</CardTitle>
|
|
<span className="text-sm text-muted-foreground">전체 {expenseItems.length}건</span>
|
|
{!isViewMode && selectedCount > 0 && (
|
|
<>
|
|
<span className="text-sm text-muted-foreground">/</span>
|
|
<span className="text-sm text-primary font-medium">{selectedCount}개 항목 선택됨</span>
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
size="sm"
|
|
className="bg-gray-900 hover:bg-gray-800"
|
|
onClick={onRemoveSelected}
|
|
>
|
|
선택삭제
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
{!isViewMode && (
|
|
<div className="flex items-center gap-2">
|
|
<QuantityInput
|
|
min={1}
|
|
value={expenseAddCount}
|
|
onChange={(val) => setExpenseAddCount(val)}
|
|
className="w-16 text-center"
|
|
id="expense-add-count"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
size="sm"
|
|
className="bg-gray-900 hover:bg-gray-800"
|
|
onClick={() => {
|
|
const count = Math.max(1, expenseAddCount ?? 1);
|
|
onAddItems(count);
|
|
}}
|
|
>
|
|
추가
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-gray-100">
|
|
{!isViewMode && (
|
|
<TableHead className="w-[50px] text-center">
|
|
<input
|
|
type="checkbox"
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
checked={allSelected}
|
|
onChange={(e) => onSelectAll(e.target.checked)}
|
|
/>
|
|
</TableHead>
|
|
)}
|
|
<TableHead className="text-center">공과</TableHead>
|
|
<TableHead className="text-right">금액</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{expenseItems.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={isViewMode ? 2 : 3}
|
|
className="text-center text-gray-500 py-8"
|
|
>
|
|
등록된 공과가 없습니다.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
expenseItems.map((item) => (
|
|
<TableRow key={item.id}>
|
|
{!isViewMode && (
|
|
<TableCell className="text-center">
|
|
<input
|
|
type="checkbox"
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
checked={item.selected || false}
|
|
onChange={(e) => onSelectItem(item.id, e.target.checked)}
|
|
/>
|
|
</TableCell>
|
|
)}
|
|
<TableCell>
|
|
<Select
|
|
value={item.name}
|
|
onValueChange={(val) => onItemChange(item.id, 'name', val)}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger className={isViewMode ? 'bg-gray-50' : 'bg-white'}>
|
|
<SelectValue placeholder="공과 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{expenseOptions.length === 0 ? (
|
|
<SelectItem value="_empty" disabled>
|
|
등록된 공과 품목이 없습니다
|
|
</SelectItem>
|
|
) : (
|
|
expenseOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</TableCell>
|
|
<TableCell>
|
|
<CurrencyInput
|
|
value={item.amount}
|
|
onChange={(value) => onItemChange(item.id, 'amount', value ?? 0)}
|
|
disabled={isViewMode}
|
|
className={`text-right ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
{/* 합계 행 */}
|
|
{expenseItems.length > 0 && (
|
|
<TableRow className="bg-gray-50 font-medium">
|
|
<TableCell colSpan={isViewMode ? 1 : 2} className="text-center">
|
|
합계
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatAmount(expenseItems.reduce((sum, item) => sum + item.amount, 0))}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |