chore(WEB): CEO 대시보드 개선 및 모바일 테스트 계획 추가

- CEO 대시보드: 일일보고, 접대비, 복리후생 섹션 개선
- CEO 대시보드: 상세 모달 기능 확장
- 카드거래조회: 기능 및 타입 확장
- 알림설정: 항목 설정 다이얼로그 추가
- 회사정보관리: 컴포넌트 개선
- 모바일 오버플로우 테스트 계획서 추가 (Galaxy Fold 대응)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-09 16:02:04 +09:00
parent f92393f898
commit e4af3232dd
13 changed files with 1924 additions and 199 deletions

View File

@@ -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>
</>
);
}

View File

@@ -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: '미설정' },