chore(WEB): CEO 대시보드 개선 및 모바일 테스트 계획 추가
- CEO 대시보드: 일일보고, 접대비, 복리후생 섹션 개선 - CEO 대시보드: 상세 모달 기능 확장 - 카드거래조회: 기능 및 타입 확장 - 알림설정: 항목 설정 다이얼로그 추가 - 회사정보관리: 컴포넌트 개선 - 모바일 오버플로우 테스트 계획서 추가 (Galaxy Fold 대응) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -49,6 +51,7 @@ import type {
|
||||
import {
|
||||
SORT_OPTIONS,
|
||||
ACCOUNT_SUBJECT_OPTIONS,
|
||||
USAGE_TYPE_OPTIONS,
|
||||
} from './types';
|
||||
import { getCardTransactionList, getCardTransactionSummary, bulkUpdateAccountCode } from './actions';
|
||||
|
||||
@@ -90,6 +93,15 @@ export function CardTransactionInquiry({
|
||||
// 선택 필요 알림 다이얼로그
|
||||
const [showSelectWarningDialog, setShowSelectWarningDialog] = useState(false);
|
||||
|
||||
// 상세 모달 상태
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<CardTransaction | null>(null);
|
||||
const [detailFormData, setDetailFormData] = useState({
|
||||
memo: '',
|
||||
usageType: 'unset',
|
||||
});
|
||||
const [isDetailSaving, setIsDetailSaving] = useState(false);
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd'));
|
||||
const [endDate, setEndDate] = useState(() => format(endOfMonth(new Date()), 'yyyy-MM-dd'));
|
||||
@@ -152,6 +164,40 @@ export function CardTransactionInquiry({
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
// ===== 상세 모달 핸들러 =====
|
||||
const handleRowClick = useCallback((item: CardTransaction) => {
|
||||
setSelectedItem(item);
|
||||
setDetailFormData({
|
||||
memo: item.memo || '',
|
||||
usageType: item.usageType || 'unset',
|
||||
});
|
||||
setShowDetailModal(true);
|
||||
}, []);
|
||||
|
||||
const handleDetailSave = useCallback(async () => {
|
||||
if (!selectedItem) return;
|
||||
|
||||
setIsDetailSaving(true);
|
||||
try {
|
||||
// TODO: API 호출로 상세 정보 저장
|
||||
// const result = await updateCardTransaction(selectedItem.id, detailFormData);
|
||||
|
||||
// 임시: 로컬 데이터 업데이트
|
||||
setData(prev => prev.map(item =>
|
||||
item.id === selectedItem.id
|
||||
? { ...item, memo: detailFormData.memo, usageType: detailFormData.usageType }
|
||||
: item
|
||||
));
|
||||
|
||||
setShowDetailModal(false);
|
||||
setSelectedItem(null);
|
||||
} catch (error) {
|
||||
console.error('[CardTransactionInquiry] handleDetailSave error:', error);
|
||||
} finally {
|
||||
setIsDetailSaving(false);
|
||||
}
|
||||
}, [selectedItem, detailFormData]);
|
||||
|
||||
// ===== 체크박스 핸들러 =====
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
setSelectedItems(prev => {
|
||||
@@ -269,6 +315,11 @@ export function CardTransactionInquiry({
|
||||
];
|
||||
}, [summary]);
|
||||
|
||||
// ===== 사용유형 라벨 변환 함수 =====
|
||||
const getUsageTypeLabel = useCallback((value: string) => {
|
||||
return USAGE_TYPE_OPTIONS.find(opt => opt.value === value)?.label || '미설정';
|
||||
}, []);
|
||||
|
||||
// ===== 테이블 컬럼 (체크박스/번호 없음) =====
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: 'card', label: '카드' },
|
||||
@@ -277,6 +328,7 @@ export function CardTransactionInquiry({
|
||||
{ key: 'usedAt', label: '사용일시' },
|
||||
{ key: 'merchantName', label: '가맹점명' },
|
||||
{ key: 'amount', label: '사용금액', className: 'text-right' },
|
||||
{ key: 'usageType', label: '사용유형' },
|
||||
], []);
|
||||
|
||||
// ===== 테이블 행 렌더링 =====
|
||||
@@ -286,7 +338,8 @@ export function CardTransactionInquiry({
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="hover:bg-muted/50"
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
{/* 체크박스 */}
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
@@ -306,9 +359,11 @@ export function CardTransactionInquiry({
|
||||
<TableCell className="text-right font-medium">
|
||||
{item.amount.toLocaleString()}
|
||||
</TableCell>
|
||||
{/* 사용유형 */}
|
||||
<TableCell>{getUsageTypeLabel(item.usageType)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}, [selectedItems, toggleSelection]);
|
||||
}, [selectedItems, toggleSelection, getUsageTypeLabel, handleRowClick]);
|
||||
|
||||
// ===== 모바일 카드 렌더링 =====
|
||||
const renderMobileCard = useCallback((
|
||||
@@ -430,6 +485,7 @@ export function CardTransactionInquiry({
|
||||
<TableCell className="text-right font-bold">
|
||||
{totalAmount.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -519,6 +575,94 @@ export function CardTransactionInquiry({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 카드 내역 상세 모달 */}
|
||||
<Dialog open={showDetailModal} onOpenChange={setShowDetailModal}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>카드 내역 상세</DialogTitle>
|
||||
<DialogDescription>
|
||||
카드 사용 상세 내역을 등록합니다
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedItem && (
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<h4 className="font-medium text-gray-800 mb-4">기본 정보</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-sm text-gray-500">사용일시</Label>
|
||||
<p className="mt-1 text-sm font-medium">{selectedItem.usedAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm text-gray-500">카드</Label>
|
||||
<p className="mt-1 text-sm font-medium">{selectedItem.card} ({selectedItem.cardName})</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm text-gray-500">사용자</Label>
|
||||
<p className="mt-1 text-sm font-medium">{selectedItem.user}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm text-gray-500">사용금액</Label>
|
||||
<p className="mt-1 text-sm font-medium">{selectedItem.amount.toLocaleString()}원</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="detail-memo" className="text-sm text-gray-500">적요</Label>
|
||||
<Input
|
||||
id="detail-memo"
|
||||
value={detailFormData.memo}
|
||||
onChange={(e) => setDetailFormData(prev => ({ ...prev, memo: e.target.value }))}
|
||||
placeholder="적요"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm text-gray-500">가맹점</Label>
|
||||
<p className="mt-1 text-sm font-medium">{selectedItem.merchantName}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="detail-usage-type" className="text-sm text-gray-500">사용 유형</Label>
|
||||
<Select
|
||||
key={`usage-type-${detailFormData.usageType}`}
|
||||
value={detailFormData.usageType}
|
||||
onValueChange={(value) => setDetailFormData(prev => ({ ...prev, usageType: value }))}
|
||||
>
|
||||
<SelectTrigger id="detail-usage-type" className="mt-1">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{USAGE_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleDetailSave}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
disabled={isDetailSaving}
|
||||
>
|
||||
{isDetailSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
'수정'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,8 @@ export interface CardTransaction {
|
||||
usedAt: string; // 사용일시
|
||||
merchantName: string; // 가맹점명
|
||||
amount: number; // 사용금액
|
||||
memo?: string; // 적요
|
||||
usageType: string; // 사용유형
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -25,6 +27,28 @@ export const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
||||
{ value: 'amountLow', label: '금액낮은순' },
|
||||
];
|
||||
|
||||
// ===== 사용유형 옵션 =====
|
||||
export const USAGE_TYPE_OPTIONS = [
|
||||
{ value: 'unset', label: '미설정' },
|
||||
{ value: 'welfare', label: '복리후생비' },
|
||||
{ value: 'entertainment', label: '접대비' },
|
||||
{ value: 'transportation', label: '여비교통비' },
|
||||
{ value: 'vehicle', label: '차량유지비' },
|
||||
{ value: 'supplies', label: '소모품비' },
|
||||
{ value: 'delivery', label: '운반비' },
|
||||
{ value: 'communication', label: '통신비' },
|
||||
{ value: 'printing', label: '도서인쇄비' },
|
||||
{ value: 'training', label: '교육훈련비' },
|
||||
{ value: 'insurance', label: '보험료' },
|
||||
{ value: 'advertising', label: '광고선전비' },
|
||||
{ value: 'membership', label: '회비' },
|
||||
{ value: 'commission', label: '지급수수료' },
|
||||
{ value: 'taxesAndDues', label: '세금과공과' },
|
||||
{ value: 'repair', label: '수선비' },
|
||||
{ value: 'rent', label: '임차료' },
|
||||
{ value: 'miscellaneous', label: '잡비' },
|
||||
];
|
||||
|
||||
// ===== 계정과목명 옵션 (상단 셀렉트) =====
|
||||
export const ACCOUNT_SUBJECT_OPTIONS = [
|
||||
{ value: 'unset', label: '미설정' },
|
||||
|
||||
Reference in New Issue
Block a user