- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등 - 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리 - 설정 모듈: 계정관리/직급/직책/권한 상세 간소화 - 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리 - UniversalListPage 엑셀 다운로드 및 필터 기능 확장 - 대시보드/게시판/수주 등 날짜 유틸 공통화 적용 - claudedocs 문서 인덱스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 카드 내역 불러오기 팝업 (팝업 in 팝업)
|
|
*
|
|
* - ManualEntryModal 위에 z-index로 표시
|
|
* - 날짜범위 + 가맹점/승인번호 검색 + 조회
|
|
* - 빈 상태: "카드 내역이 없습니다."
|
|
* - 테이블: 날짜, 가맹점, 금액, 승인번호, 선택 버튼
|
|
* - 선택 시 부모에 데이터 전달 후 닫기
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { formatNumber } from '@/lib/utils/amount';
|
|
import { Search } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { getTodayString, getLocalDateString } from '@/lib/utils/date';
|
|
import { getCardHistory } from './actions';
|
|
import type { CardHistoryRecord } from './types';
|
|
|
|
interface CardHistoryModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSelect: (record: CardHistoryRecord) => void;
|
|
}
|
|
|
|
export function CardHistoryModal({
|
|
open,
|
|
onOpenChange,
|
|
onSelect,
|
|
}: CardHistoryModalProps) {
|
|
const today = getTodayString();
|
|
const monthAgo = getLocalDateString(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
|
|
|
const [startDate, setStartDate] = useState(monthAgo);
|
|
const [endDate, setEndDate] = useState(today);
|
|
const [searchText, setSearchText] = useState('');
|
|
const [data, setData] = useState<CardHistoryRecord[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [hasSearched, setHasSearched] = useState(false);
|
|
|
|
const handleSearch = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setHasSearched(true);
|
|
try {
|
|
const result = await getCardHistory({
|
|
startDate,
|
|
endDate,
|
|
search: searchText,
|
|
page: 1,
|
|
perPage: 50,
|
|
});
|
|
if (result.success) {
|
|
setData(result.data ?? []);
|
|
} else {
|
|
toast.error(result.error || '카드 내역 조회에 실패했습니다.');
|
|
}
|
|
} catch {
|
|
toast.error('서버 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [startDate, endDate, searchText]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[600px] max-h-[80vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle>카드 내역 불러오기</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{/* 검색 영역 */}
|
|
<div className="space-y-2">
|
|
{/* Row1: 날짜 범위 */}
|
|
<div className="flex items-center gap-2">
|
|
<DatePicker
|
|
value={startDate}
|
|
onChange={setStartDate}
|
|
className="flex-1"
|
|
/>
|
|
<span className="text-sm text-muted-foreground shrink-0">~</span>
|
|
<DatePicker
|
|
value={endDate}
|
|
onChange={setEndDate}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
{/* Row2: 검색어 + 조회 */}
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
placeholder="가맹점/승인번호"
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
className="flex-1 h-9 text-sm"
|
|
/>
|
|
<Button size="sm" onClick={handleSearch} disabled={isLoading}>
|
|
<Search className="h-4 w-4 mr-1" />
|
|
조회
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 영역 */}
|
|
<div className="flex-1 overflow-auto border rounded-md">
|
|
{!hasSearched ? (
|
|
<div className="flex items-center justify-center h-[200px] text-sm text-muted-foreground">
|
|
조회 버튼을 클릭하여 카드 내역을 검색하세요.
|
|
</div>
|
|
) : isLoading ? (
|
|
<div className="flex items-center justify-center h-[200px] text-sm text-muted-foreground">
|
|
조회 중...
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<div className="flex items-center justify-center h-[200px] text-sm text-muted-foreground">
|
|
카드 내역이 없습니다.
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-center">날짜</TableHead>
|
|
<TableHead>가맹점</TableHead>
|
|
<TableHead className="text-right">금액</TableHead>
|
|
<TableHead className="text-center">승인번호</TableHead>
|
|
<TableHead className="text-center w-[70px]">선택</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-center text-sm">
|
|
{item.transactionDate}
|
|
</TableCell>
|
|
<TableCell className="text-sm">{item.merchantName}</TableCell>
|
|
<TableCell className="text-right text-sm font-medium">
|
|
{formatNumber(item.amount)}원
|
|
</TableCell>
|
|
<TableCell className="text-center text-sm text-muted-foreground">
|
|
{item.approvalNumber}
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 px-2 text-xs"
|
|
onClick={() => onSelect(item)}
|
|
>
|
|
선택
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|