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: '미설정' },
|
||||
|
||||
@@ -281,6 +281,7 @@ const mockData: CEODashboardData = {
|
||||
],
|
||||
},
|
||||
],
|
||||
detailButtonPath: '/accounting/receivables-status',
|
||||
},
|
||||
debtCollection: {
|
||||
cards: [
|
||||
@@ -955,12 +956,40 @@ export function CEODashboard() {
|
||||
cm4: {
|
||||
title: '대표자 종합소득세 예상 가중 상세',
|
||||
summaryCards: [
|
||||
{ label: '합계', value: 3123000, unit: '원' },
|
||||
{ label: '전월 대비', value: '+12.5%', isComparison: true, isPositive: false },
|
||||
{ label: '대표자 종합세 예상 가중', value: 3123000, unit: '원' },
|
||||
{ label: '추가 세금', value: '+12.5%', isComparison: true, isPositive: false },
|
||||
{ label: '가지급금', value: '4.5억원' },
|
||||
{ label: '인정 이자', value: 6000000, unit: '원' },
|
||||
{ label: '인정이자 4.6%', value: 6000000, unit: '원' },
|
||||
],
|
||||
table: {
|
||||
comparisonSection: {
|
||||
leftBox: {
|
||||
title: '가지급금 인정이자가 반영된 종합소득세',
|
||||
items: [
|
||||
{ label: '현재 예상 과세표준 (근로소득+상여)', value: 6000000, unit: '원' },
|
||||
{ label: '현재 적용 세율', value: '19%' },
|
||||
{ label: '현재 예상 세액', value: 10000000, unit: '원' },
|
||||
],
|
||||
borderColor: 'orange',
|
||||
},
|
||||
rightBox: {
|
||||
title: '가지급금 인정이자가 정리된 종합소득세',
|
||||
items: [
|
||||
{ label: '가지급금 정리 시 예상 과세표준 (근로소득+상여)', value: 6000000, unit: '원' },
|
||||
{ label: '가지급금 정리 시 적용 세율', value: '19%' },
|
||||
{ label: '가지급금 정리 시 예상 세액', value: 10000000, unit: '원' },
|
||||
],
|
||||
borderColor: 'blue',
|
||||
},
|
||||
vsLabel: '종합소득세 예상 절감',
|
||||
vsValue: 3123000,
|
||||
vsSubLabel: '감소 세금 -12.5%',
|
||||
vsBreakdown: [
|
||||
{ label: '종합소득세', value: -2000000, unit: '원' },
|
||||
{ label: '지방소득세', value: -200000, unit: '원' },
|
||||
{ label: '4대 보험', value: -1000000, unit: '원' },
|
||||
],
|
||||
},
|
||||
referenceTable: {
|
||||
title: '종합소득세 과세표준 (2024년 기준)',
|
||||
columns: [
|
||||
{ key: 'bracket', label: '과세표준', align: 'left' },
|
||||
@@ -989,16 +1018,450 @@ export function CEODashboard() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 접대비 클릭
|
||||
const handleEntertainmentClick = useCallback(() => {
|
||||
// TODO: 접대비 상세 팝업 열기
|
||||
console.log('접대비 클릭');
|
||||
// 접대비 현황 카드 클릭 (개별 카드 클릭 시 상세 모달)
|
||||
const handleEntertainmentCardClick = useCallback((cardId: string) => {
|
||||
// 접대비 상세 공통 모달 config (et2, et3, et4 공통)
|
||||
const entertainmentDetailConfig: DetailModalConfig = {
|
||||
title: '접대비 상세',
|
||||
summaryCards: [
|
||||
// 첫 번째 줄: 당해년도
|
||||
{ label: '당해년도 접대비 총한도', value: 3123000, unit: '원' },
|
||||
{ label: '당해년도 접대비 잔여한도', value: 6000000, unit: '원' },
|
||||
{ label: '당해년도 접대비 사용금액', value: 6000000, unit: '원' },
|
||||
{ label: '당해년도 접대비 사용잔액', value: 0, unit: '원' },
|
||||
// 두 번째 줄: 분기별
|
||||
{ label: '1사분기 접대비 총한도', value: 3123000, unit: '원' },
|
||||
{ label: '1사분기 접대비 잔여한도', value: 6000000, unit: '원' },
|
||||
{ label: '1사분기 접대비 사용금액', value: 6000000, unit: '원' },
|
||||
{ label: '1사분기 접대비 초과금액', value: 6000000, unit: '원' },
|
||||
],
|
||||
barChart: {
|
||||
title: '월별 접대비 사용 추이',
|
||||
data: [
|
||||
{ name: '1월', value: 3500000 },
|
||||
{ name: '2월', value: 4200000 },
|
||||
{ name: '3월', value: 2300000 },
|
||||
{ name: '4월', value: 3800000 },
|
||||
{ name: '5월', value: 4500000 },
|
||||
{ name: '6월', value: 3200000 },
|
||||
{ name: '7월', value: 2800000 },
|
||||
],
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#60A5FA',
|
||||
},
|
||||
pieChart: {
|
||||
title: '사용자별 접대비 사용 비율',
|
||||
data: [
|
||||
{ name: '홍길동', value: 15000000, percentage: 53, color: '#60A5FA' },
|
||||
{ name: '김철수', value: 10000000, percentage: 31, color: '#34D399' },
|
||||
{ name: '이영희', value: 10000000, percentage: 10, color: '#FBBF24' },
|
||||
{ name: '기타', value: 2000000, percentage: 6, color: '#F87171' },
|
||||
],
|
||||
},
|
||||
table: {
|
||||
title: '월별 접대비 사용 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'cardName', label: '카드명', align: 'left' },
|
||||
{ key: 'user', label: '사용자', align: 'center' },
|
||||
{ key: 'useDate', label: '사용일시', align: 'center', format: 'date' },
|
||||
{ key: 'transDate', label: '거래일시', align: 'center', format: 'date' },
|
||||
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
|
||||
{ key: 'purpose', label: '사용용도', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, purpose: '사용용도' },
|
||||
{ cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, purpose: '사용용도' },
|
||||
{ cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, purpose: '사용용도' },
|
||||
{ cardName: '카드명', user: '홍길동', useDate: '2025-10-14 12:12', transDate: '가맹점명', amount: 1000000, purpose: '사용용도' },
|
||||
{ cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, purpose: '사용용도' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'user',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '홍길동', label: '홍길동' },
|
||||
{ value: '김철수', label: '김철수' },
|
||||
{ value: '이영희', label: '이영희' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 11000000,
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
// 접대비 손금한도 계산 - 기본한도 / 수입금액별 추가한도
|
||||
referenceTables: [
|
||||
{
|
||||
title: '접대비 손금한도 계산 - 기본한도',
|
||||
columns: [
|
||||
{ key: 'type', label: '구분', align: 'left' },
|
||||
{ key: 'limit', label: '기본한도', align: 'right' },
|
||||
],
|
||||
data: [
|
||||
{ type: '일반법인', limit: '3,600만원 (연 1,200만원)' },
|
||||
{ type: '중소기업', limit: '5,400만원 (연 3,600만원)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '수입금액별 추가한도',
|
||||
columns: [
|
||||
{ key: 'range', label: '수입금액', align: 'left' },
|
||||
{ key: 'rate', label: '적용률', align: 'center' },
|
||||
],
|
||||
data: [
|
||||
{ range: '100억원 이하', rate: '0.3%' },
|
||||
{ range: '100억원 초과 ~ 500억원 이하', rate: '0.2%' },
|
||||
{ range: '500억원 초과', rate: '0.03%' },
|
||||
],
|
||||
},
|
||||
],
|
||||
// 접대비 계산
|
||||
calculationCards: {
|
||||
title: '접대비 계산',
|
||||
cards: [
|
||||
{ label: '기본한도', value: 36000000 },
|
||||
{ label: '추가한도', value: 91170000, operator: '+' },
|
||||
{ label: '접대비 손금한도', value: 127170000, operator: '=' },
|
||||
],
|
||||
},
|
||||
// 접대비 현황 (분기별)
|
||||
quarterlyTable: {
|
||||
title: '접대비 현황',
|
||||
rows: [
|
||||
{ label: '접대비 한도', q1: 31792500, q2: 31792500, q3: 31792500, q4: 31792500, total: 127170000 },
|
||||
{ label: '접대비 사용', q1: 10000000, q2: 0, q3: 0, q4: 0, total: 10000000 },
|
||||
{ label: '접대비 잔여', q1: 21792500, q2: 31792500, q3: 31792500, q4: 31792500, total: 117170000 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const cardConfigs: Record<string, DetailModalConfig> = {
|
||||
et1: {
|
||||
title: '당해 매출 상세',
|
||||
summaryCards: [
|
||||
{ label: '당해년도 매출', value: 600000000, unit: '원' },
|
||||
{ label: '전년 대비', value: '-12.5%', isComparison: true, isPositive: false },
|
||||
{ label: '당월 매출', value: 6000000, unit: '원' },
|
||||
],
|
||||
barChart: {
|
||||
title: '월별 매출 추이',
|
||||
data: [
|
||||
{ name: '1월', value: 85000000 },
|
||||
{ name: '2월', value: 92000000 },
|
||||
{ name: '3월', value: 78000000 },
|
||||
{ name: '4월', value: 95000000 },
|
||||
{ name: '5월', value: 88000000 },
|
||||
{ name: '6월', value: 102000000 },
|
||||
{ name: '7월', value: 60000000 },
|
||||
],
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#60A5FA',
|
||||
},
|
||||
horizontalBarChart: {
|
||||
title: '당해년도 거래처별 매출',
|
||||
data: [
|
||||
{ name: '(주)세우', value: 120000000 },
|
||||
{ name: '대한건설', value: 95000000 },
|
||||
{ name: '삼성테크', value: 78000000 },
|
||||
{ name: '현대상사', value: 65000000 },
|
||||
{ name: '기타', value: 42000000 },
|
||||
],
|
||||
color: '#60A5FA',
|
||||
},
|
||||
table: {
|
||||
title: '일별 매출 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'date', label: '매출일', align: 'center', format: 'date' },
|
||||
{ key: 'vendor', label: '거래처', align: 'left' },
|
||||
{ key: 'amount', label: '매출금액', align: 'right', format: 'currency' },
|
||||
{ key: 'type', label: '매출유형', align: 'center', highlightValue: '미설정' },
|
||||
],
|
||||
data: [
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '상품 매출' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '부품 매출' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '공사 매출' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' },
|
||||
{ date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '상품 매출' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'type',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '상품 매출', label: '상품 매출' },
|
||||
{ value: '부품 매출', label: '부품 매출' },
|
||||
{ value: '공사 매출', label: '공사 매출' },
|
||||
{ value: '임대 수익', label: '임대 수익' },
|
||||
{ value: '기타 매출', label: '기타 매출' },
|
||||
{ value: '미설정', label: '미설정' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 111000000,
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
},
|
||||
// et2, et3, et4는 모두 동일한 접대비 상세 모달
|
||||
et2: entertainmentDetailConfig,
|
||||
et3: entertainmentDetailConfig,
|
||||
et4: entertainmentDetailConfig,
|
||||
};
|
||||
|
||||
const config = cardConfigs[cardId];
|
||||
if (config) {
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 부가세 클릭
|
||||
// 복리후생비 현황 카드 클릭 (모든 카드가 동일한 상세 모달)
|
||||
const handleWelfareCardClick = useCallback(() => {
|
||||
// 계산 방식에 따른 조건부 calculationCards 생성
|
||||
const calculationType = dashboardSettings.welfare.calculationType;
|
||||
const calculationCards = calculationType === 'fixed'
|
||||
? {
|
||||
// 직원당 정액 금액/월 방식
|
||||
title: '복리후생비 계산',
|
||||
subtitle: '직원당 정액 금액/월 200,000원',
|
||||
cards: [
|
||||
{ label: '직원 수', value: 20, unit: '명' },
|
||||
{ label: '연간 직원당 월급 금액', value: 2400000, unit: '원', operator: '×' as const },
|
||||
{ label: '당해년도 복리후생비 총 한도', value: 48000000, unit: '원', operator: '=' as const },
|
||||
],
|
||||
}
|
||||
: {
|
||||
// 연봉 총액 비율 방식
|
||||
title: '복리후생비 계산',
|
||||
subtitle: '연봉 총액 기준 비율 20.5%',
|
||||
cards: [
|
||||
{ label: '연봉 총액', value: 1000000000, unit: '원' },
|
||||
{ label: '비율', value: 20.5, unit: '%', operator: '×' as const },
|
||||
{ label: '당해년도 복리후생비 총 한도', value: 205000000, unit: '원', operator: '=' as const },
|
||||
],
|
||||
};
|
||||
|
||||
const config: DetailModalConfig = {
|
||||
title: '복리후생비 상세',
|
||||
summaryCards: [
|
||||
// 1행: 당해년도 기준
|
||||
{ label: '당해년도 복리후생비 계정', value: 3123000, unit: '원' },
|
||||
{ label: '당해년도 복리후생비 한도', value: 600000, unit: '원' },
|
||||
{ label: '당해년도 복리후생비 사용', value: 6000000, unit: '원' },
|
||||
{ label: '당해년도 잔여한도', value: 0, unit: '원' },
|
||||
// 2행: 1사분기 기준
|
||||
{ label: '1사분기 복리후생비 총 한도', value: 3123000, unit: '원' },
|
||||
{ label: '1사분기 복리후생비 잔여한도', value: 6000000, unit: '원' },
|
||||
{ label: '1사분기 복리후생비 사용금액', value: 6000000, unit: '원' },
|
||||
{ label: '1사분기 복리후생비 초과 금액', value: 6000000, unit: '원' },
|
||||
],
|
||||
barChart: {
|
||||
title: '월별 복리후생비 사용 추이',
|
||||
data: [
|
||||
{ name: '1월', value: 1500000 },
|
||||
{ name: '2월', value: 1800000 },
|
||||
{ name: '3월', value: 2200000 },
|
||||
{ name: '4월', value: 1900000 },
|
||||
{ name: '5월', value: 2100000 },
|
||||
{ name: '6월', value: 1700000 },
|
||||
],
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#60A5FA',
|
||||
},
|
||||
pieChart: {
|
||||
title: '항목별 사용 비율',
|
||||
data: [
|
||||
{ name: '식비', value: 55000000, percentage: 55, color: '#FBBF24' },
|
||||
{ name: '건강검진', value: 25000000, percentage: 5, color: '#60A5FA' },
|
||||
{ name: '경조사비', value: 10000000, percentage: 10, color: '#F87171' },
|
||||
{ name: '기타', value: 10000000, percentage: 30, color: '#34D399' },
|
||||
],
|
||||
},
|
||||
table: {
|
||||
title: '일별 복리후생비 사용 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'cardName', label: '카드명', align: 'left' },
|
||||
{ key: 'user', label: '사용자', align: 'center' },
|
||||
{ key: 'date', label: '사용일자', align: 'center', format: 'date' },
|
||||
{ key: 'store', label: '가맹점명', align: 'left' },
|
||||
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
|
||||
{ key: 'usageType', label: '사용항목', align: 'center' },
|
||||
],
|
||||
data: [
|
||||
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, usageType: '식비' },
|
||||
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1200000, usageType: '건강검진' },
|
||||
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1500000, usageType: '경조사비' },
|
||||
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1300000, usageType: '기타' },
|
||||
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 6000000, usageType: '식비' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'usageType',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '식비', label: '식비' },
|
||||
{ value: '건강검진', label: '건강검진' },
|
||||
{ value: '경조사비', label: '경조사비' },
|
||||
{ value: '기타', label: '기타' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 11000000,
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
// 복리후생비 계산 (조건부 - calculationType에 따라)
|
||||
calculationCards,
|
||||
// 복리후생비 현황 (분기별 테이블)
|
||||
quarterlyTable: {
|
||||
title: '복리후생비 현황',
|
||||
rows: [
|
||||
{ label: '한도금액', q1: 12000000, q2: 12000000, q3: 12000000, q4: 12000000, total: 48000000 },
|
||||
{ label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' },
|
||||
{ label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' },
|
||||
{ label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' },
|
||||
{ label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}, [dashboardSettings.welfare.calculationType]);
|
||||
|
||||
// 부가세 클릭 (모든 카드가 동일한 상세 모달)
|
||||
const handleVatClick = useCallback(() => {
|
||||
// TODO: 부가세 상세 팝업 열기
|
||||
console.log('부가세 클릭');
|
||||
const config: DetailModalConfig = {
|
||||
title: '예상 납부세액',
|
||||
summaryCards: [],
|
||||
// 세액 산출 내역 테이블
|
||||
referenceTable: {
|
||||
title: '2026년 1사분기 세액 산출 내역',
|
||||
columns: [
|
||||
{ key: 'category', label: '구분', align: 'center' },
|
||||
{ key: 'amount', label: '금액', align: 'right' },
|
||||
{ key: 'note', label: '비고', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ category: '매출세액', amount: '11,000,000', note: '과세매출 X 10%' },
|
||||
{ category: '매입세액', amount: '1,000,000', note: '공제대상 매입 X 10%' },
|
||||
{ category: '경감·공제세액', amount: '0', note: '해당없음' },
|
||||
],
|
||||
},
|
||||
// 예상 납부세액 계산
|
||||
calculationCards: {
|
||||
title: '예상 납부세액 계산',
|
||||
cards: [
|
||||
{ label: '매출세액', value: 11000000, unit: '원' },
|
||||
{ label: '매입세액', value: 1000000, unit: '원', operator: '-' },
|
||||
{ label: '경감·공제세액', value: 0, unit: '원', operator: '-' },
|
||||
{ label: '예상 납부세액', value: 10000000, unit: '원', operator: '=' },
|
||||
],
|
||||
},
|
||||
// 세금계산서 미발행/미수취 내역
|
||||
table: {
|
||||
title: '세금계산서 미발행/미수취 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'type', label: '구분', align: 'center' },
|
||||
{ key: 'issueDate', label: '발행일자', align: 'center', format: 'date' },
|
||||
{ key: 'vendor', label: '거래처', align: 'left' },
|
||||
{ key: 'vat', label: '부가세', align: 'right', format: 'currency' },
|
||||
{ key: 'invoiceStatus', label: '세금계산서 발행', align: 'center' },
|
||||
],
|
||||
data: [
|
||||
{ type: '매출', issueDate: '2025-12-12', vendor: '거래처1', vat: 11000000, invoiceStatus: '미발행' },
|
||||
{ type: '매입', issueDate: '2025-12-12', vendor: '거래처2', vat: 11000000, invoiceStatus: '미수취' },
|
||||
{ type: '매출', issueDate: '2025-12-12', vendor: '거래처3', vat: 11000000, invoiceStatus: '미발행' },
|
||||
{ type: '매입', issueDate: '2025-12-12', vendor: '거래처4', vat: 11000000, invoiceStatus: '미수취' },
|
||||
{ type: '매출', issueDate: '2025-12-12', vendor: '거래처5', vat: 11000000, invoiceStatus: '미발행' },
|
||||
{ type: '매입', issueDate: '2025-12-12', vendor: '거래처6', vat: 11000000, invoiceStatus: '미수취' },
|
||||
{ type: '매출', issueDate: '2025-12-12', vendor: '거래처7', vat: 11000000, invoiceStatus: '미발행' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'type',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '매출', label: '매출' },
|
||||
{ value: '매입', label: '매입' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'invoiceStatus',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '미발행', label: '미발행' },
|
||||
{ value: '미수취', label: '미수취' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 111000000,
|
||||
totalColumnKey: 'vat',
|
||||
},
|
||||
};
|
||||
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// 캘린더 일정 클릭 (기존 일정 수정)
|
||||
@@ -1119,13 +1582,16 @@ export function CEODashboard() {
|
||||
{dashboardSettings.entertainment.enabled && (
|
||||
<EntertainmentSection
|
||||
data={data.entertainment}
|
||||
onClick={handleEntertainmentClick}
|
||||
onCardClick={handleEntertainmentCardClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 복리후생비 현황 */}
|
||||
{dashboardSettings.welfare.enabled && (
|
||||
<WelfareSection data={data.welfare} />
|
||||
<WelfareSection
|
||||
data={data.welfare}
|
||||
onCardClick={handleWelfareCardClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 미수금 현황 */}
|
||||
|
||||
@@ -38,6 +38,8 @@ import type {
|
||||
TableFilterConfig,
|
||||
ComparisonSectionConfig,
|
||||
ReferenceTableConfig,
|
||||
CalculationCardsConfig,
|
||||
QuarterlyTableConfig,
|
||||
} from '../types';
|
||||
|
||||
interface DetailModalProps {
|
||||
@@ -245,7 +247,7 @@ const ComparisonSection = ({ config }: { config: ComparisonSectionConfig }) => {
|
||||
{/* VS 영역 */}
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<span className="text-2xl font-bold text-gray-400 mb-2">VS</span>
|
||||
<div className="bg-red-50 rounded-lg px-4 py-2 text-center">
|
||||
<div className="bg-red-50 rounded-lg px-4 py-3 text-center min-w-[180px]">
|
||||
<p className="text-xs text-gray-600 mb-1">{config.vsLabel}</p>
|
||||
<p className="text-xl font-bold text-red-500">
|
||||
{typeof config.vsValue === 'number'
|
||||
@@ -255,6 +257,21 @@ const ComparisonSection = ({ config }: { config: ComparisonSectionConfig }) => {
|
||||
{config.vsSubLabel && (
|
||||
<p className="text-xs text-gray-500 mt-1">{config.vsSubLabel}</p>
|
||||
)}
|
||||
{/* VS 세부 항목 */}
|
||||
{config.vsBreakdown && config.vsBreakdown.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-red-200 space-y-1">
|
||||
{config.vsBreakdown.map((item, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span className="text-gray-600">{item.label}</span>
|
||||
<span className="font-medium text-gray-700">
|
||||
{typeof item.value === 'number'
|
||||
? formatCurrency(item.value) + (item.unit || '원')
|
||||
: item.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,6 +301,105 @@ const ComparisonSection = ({ config }: { config: ComparisonSectionConfig }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 계산 카드 섹션 컴포넌트 (접대비 계산 등)
|
||||
*/
|
||||
const CalculationCardsSection = ({ config }: { config: CalculationCardsConfig }) => {
|
||||
const isResultCard = (index: number, operator?: string) => {
|
||||
// '=' 연산자가 있는 카드는 결과 카드로 강조
|
||||
return operator === '=';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h4 className="font-medium text-gray-800">{config.title}</h4>
|
||||
{config.subtitle && (
|
||||
<span className="text-sm text-gray-500">{config.subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{config.cards.map((card, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
{/* 연산자 표시 (첫 번째 카드 제외) */}
|
||||
{index > 0 && card.operator && (
|
||||
<span className="text-3xl font-bold text-gray-400">
|
||||
{card.operator}
|
||||
</span>
|
||||
)}
|
||||
{/* 카드 */}
|
||||
<div className={cn(
|
||||
"rounded-lg p-5 min-w-[180px] text-center border",
|
||||
isResultCard(index, card.operator)
|
||||
? "bg-blue-50 border-blue-200"
|
||||
: "bg-gray-50 border-gray-200"
|
||||
)}>
|
||||
<p className={cn(
|
||||
"text-sm mb-2",
|
||||
isResultCard(index, card.operator) ? "text-blue-600" : "text-gray-500"
|
||||
)}>
|
||||
{card.label}
|
||||
</p>
|
||||
<p className={cn(
|
||||
"text-2xl font-bold",
|
||||
isResultCard(index, card.operator) ? "text-blue-700" : "text-gray-900"
|
||||
)}>
|
||||
{formatCurrency(card.value)}{card.unit || '원'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 분기별 테이블 섹션 컴포넌트 (접대비 현황 등)
|
||||
*/
|
||||
const QuarterlyTableSection = ({ config }: { config: QuarterlyTableConfig }) => {
|
||||
const formatValue = (value: number | string | undefined): string => {
|
||||
if (value === undefined) return '-';
|
||||
if (typeof value === 'number') return formatCurrency(value);
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-medium text-gray-800 mb-3">{config.title}</h4>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-left">구분</th>
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-center">1사분기</th>
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-center">2사분기</th>
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-center">3사분기</th>
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-center">4사분기</th>
|
||||
<th className="px-4 py-3 text-xs font-medium text-gray-600 text-center">합계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.rows.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className="border-t border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 font-medium">{row.label}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 text-center">{formatValue(row.q1)}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 text-center">{formatValue(row.q2)}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 text-center">{formatValue(row.q3)}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 text-center">{formatValue(row.q4)}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 text-center font-medium">{formatValue(row.total)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 참조 테이블 컴포넌트 (필터 없는 정보성 테이블)
|
||||
*/
|
||||
@@ -612,13 +728,32 @@ export function DetailModal({ isOpen, onClose, config }: DetailModalProps) {
|
||||
<ComparisonSection config={config.comparisonSection} />
|
||||
)}
|
||||
|
||||
{/* 참조 테이블 영역 */}
|
||||
{/* 참조 테이블 영역 (단일 - 테이블 위에 표시) */}
|
||||
{config.referenceTable && (
|
||||
<ReferenceTableSection config={config.referenceTable} />
|
||||
)}
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
{/* 계산 카드 섹션 영역 (테이블 위에 표시) */}
|
||||
{config.calculationCards && (
|
||||
<CalculationCardsSection config={config.calculationCards} />
|
||||
)}
|
||||
|
||||
{/* 메인 테이블 영역 */}
|
||||
{config.table && <TableSection key={config.title} config={config.table} />}
|
||||
|
||||
{/* 참조 테이블 영역 (다중 - 테이블 아래 표시) */}
|
||||
{config.referenceTables && config.referenceTables.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{config.referenceTables.map((tableConfig, index) => (
|
||||
<ReferenceTableSection key={index} config={tableConfig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 분기별 테이블 영역 */}
|
||||
{config.quarterlyTable && (
|
||||
<QuarterlyTableSection config={config.quarterlyTable} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -11,10 +11,7 @@ interface DailyReportSectionProps {
|
||||
|
||||
export function DailyReportSection({ data, onClick }: DailyReportSectionProps) {
|
||||
return (
|
||||
<Card
|
||||
className={onClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SectionTitle title="일일 일보" badge="info" />
|
||||
@@ -23,7 +20,7 @@ export function DailyReportSection({ data, onClick }: DailyReportSectionProps) {
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
<AmountCardItem key={card.id} card={card} onClick={onClick} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import type { EntertainmentData } from '../types';
|
||||
|
||||
interface EntertainmentSectionProps {
|
||||
data: EntertainmentData;
|
||||
onClick?: () => void;
|
||||
onCardClick?: (cardId: string) => void;
|
||||
}
|
||||
|
||||
export function EntertainmentSection({ data, onClick }: EntertainmentSectionProps) {
|
||||
export function EntertainmentSection({ data, onCardClick }: EntertainmentSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
@@ -20,7 +20,7 @@ export function EntertainmentSection({ data, onClick }: EntertainmentSectionProp
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onClick}
|
||||
onClick={() => onCardClick?.(card.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,10 @@ import type { WelfareData } from '../types';
|
||||
|
||||
interface WelfareSectionProps {
|
||||
data: WelfareData;
|
||||
onCardClick?: (cardId: string) => void;
|
||||
}
|
||||
|
||||
export function WelfareSection({ data }: WelfareSectionProps) {
|
||||
export function WelfareSection({ data, onCardClick }: WelfareSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
@@ -16,7 +17,11 @@ export function WelfareSection({ data }: WelfareSectionProps) {
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -326,6 +326,13 @@ export interface ComparisonBoxConfig {
|
||||
borderColor: 'orange' | 'blue';
|
||||
}
|
||||
|
||||
// VS 중앙 세부 항목 타입
|
||||
export interface VsBreakdownItem {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
// VS 비교 섹션 설정 타입
|
||||
export interface ComparisonSectionConfig {
|
||||
leftBox: ComparisonBoxConfig;
|
||||
@@ -333,6 +340,7 @@ export interface ComparisonSectionConfig {
|
||||
vsLabel: string;
|
||||
vsValue: string | number;
|
||||
vsSubLabel?: string;
|
||||
vsBreakdown?: VsBreakdownItem[]; // VS 중앙에 표시할 세부 항목들
|
||||
}
|
||||
|
||||
// 참조 테이블 설정 타입 (필터 없는 정보성 테이블)
|
||||
@@ -342,6 +350,37 @@ export interface ReferenceTableConfig {
|
||||
data: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
// 계산 카드 아이템 타입 (접대비 계산 등)
|
||||
export interface CalculationCardItem {
|
||||
label: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
operator?: '+' | '=' | '-' | '×'; // 연산자 표시
|
||||
}
|
||||
|
||||
// 계산 카드 섹션 설정 타입
|
||||
export interface CalculationCardsConfig {
|
||||
title: string;
|
||||
subtitle?: string; // 서브타이틀 (예: "직원당 정액 금액/월 200,000원")
|
||||
cards: CalculationCardItem[];
|
||||
}
|
||||
|
||||
// 분기별 테이블 행 타입
|
||||
export interface QuarterlyTableRow {
|
||||
label: string;
|
||||
q1?: number | string;
|
||||
q2?: number | string;
|
||||
q3?: number | string;
|
||||
q4?: number | string;
|
||||
total?: number | string;
|
||||
}
|
||||
|
||||
// 분기별 테이블 설정 타입
|
||||
export interface QuarterlyTableConfig {
|
||||
title: string;
|
||||
rows: QuarterlyTableRow[];
|
||||
}
|
||||
|
||||
// 상세 모달 전체 설정 타입
|
||||
export interface DetailModalConfig {
|
||||
title: string;
|
||||
@@ -351,6 +390,9 @@ export interface DetailModalConfig {
|
||||
horizontalBarChart?: HorizontalBarChartConfig; // 가로 막대 차트 (도넛 차트 대신 사용)
|
||||
comparisonSection?: ComparisonSectionConfig; // VS 비교 섹션
|
||||
referenceTable?: ReferenceTableConfig; // 참조 테이블 (필터 없음)
|
||||
referenceTables?: ReferenceTableConfig[]; // 다중 참조 테이블
|
||||
calculationCards?: CalculationCardsConfig; // 계산 카드 섹션
|
||||
quarterlyTable?: QuarterlyTableConfig; // 분기별 테이블
|
||||
table?: TableConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -393,8 +393,8 @@ export function CompanyInfoManagement() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 담당자명 / 담당자 연락처 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 담당자명 / 담당자 연락처 - 임시 주석처리 (추후 사용 가능) */}
|
||||
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="managerName">담당자명</Label>
|
||||
<Input
|
||||
@@ -415,7 +415,7 @@ export function CompanyInfoManagement() {
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 사업자등록증 / 사업자등록번호 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 알림설정 항목 설정 모달
|
||||
*
|
||||
* 각 알림 카테고리와 항목의 표시/숨김을 설정합니다.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import type { ItemVisibilitySettings } from './types';
|
||||
|
||||
interface ItemSettingsDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
settings: ItemVisibilitySettings;
|
||||
onSave: (settings: ItemVisibilitySettings) => void;
|
||||
}
|
||||
|
||||
// 카테고리 섹션 컴포넌트
|
||||
interface CategorySectionProps {
|
||||
title: string;
|
||||
enabled: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function CategorySection({ title, enabled, onEnabledChange, children }: CategorySectionProps) {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg overflow-hidden">
|
||||
{/* 카테고리 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-white font-medium">{title}</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={onEnabledChange}
|
||||
className="data-[state=checked]:bg-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/* 하위 항목 */}
|
||||
<div className="bg-gray-700 px-4 py-2 space-y-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 항목 행 컴포넌트
|
||||
interface ItemRowProps {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ItemRow({ label, checked, onChange, disabled }: ItemRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-gray-300 text-sm">{label}</span>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
className="data-[state=checked]:bg-blue-500 scale-90"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSettingsDialogProps) {
|
||||
const [localSettings, setLocalSettings] = useState<ItemVisibilitySettings>(settings);
|
||||
|
||||
// 모달 열릴 때 설정 동기화
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
setLocalSettings(settings);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}, [settings, onClose]);
|
||||
|
||||
// 카테고리 전체 토글
|
||||
const handleCategoryToggle = useCallback((
|
||||
category: keyof ItemVisibilitySettings,
|
||||
enabled: boolean
|
||||
) => {
|
||||
setLocalSettings(prev => {
|
||||
const categorySettings = prev[category];
|
||||
const updatedCategory: Record<string, boolean> = { enabled };
|
||||
|
||||
// 모든 하위 항목도 같이 토글
|
||||
Object.keys(categorySettings).forEach(key => {
|
||||
if (key !== 'enabled') {
|
||||
updatedCategory[key] = enabled;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[category]: updatedCategory as typeof categorySettings,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 개별 항목 토글
|
||||
const handleItemToggle = useCallback((
|
||||
category: keyof ItemVisibilitySettings,
|
||||
item: string,
|
||||
checked: boolean
|
||||
) => {
|
||||
setLocalSettings(prev => ({
|
||||
...prev,
|
||||
[category]: {
|
||||
...prev[category],
|
||||
[item]: checked,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 저장
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(localSettings);
|
||||
onClose();
|
||||
}, [localSettings, onSave, onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="!w-[400px] !max-w-[400px] max-h-[80vh] overflow-y-auto p-0 bg-gray-900 border-gray-700">
|
||||
{/* 헤더 */}
|
||||
<DialogHeader className="sticky top-0 bg-gray-900 z-10 px-4 py-3 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-white font-medium">항목 설정</DialogTitle>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-800 rounded transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{/* 공지 알림 */}
|
||||
<CategorySection
|
||||
title="공지 알림"
|
||||
enabled={localSettings.notice.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('notice', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="공지사항 알림"
|
||||
checked={localSettings.notice.notice}
|
||||
onChange={(checked) => handleItemToggle('notice', 'notice', checked)}
|
||||
disabled={!localSettings.notice.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="이벤트 알림"
|
||||
checked={localSettings.notice.event}
|
||||
onChange={(checked) => handleItemToggle('notice', 'event', checked)}
|
||||
disabled={!localSettings.notice.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 일정 알림 */}
|
||||
<CategorySection
|
||||
title="일정 알림"
|
||||
enabled={localSettings.schedule.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('schedule', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="부가세 신고 알림"
|
||||
checked={localSettings.schedule.vatReport}
|
||||
onChange={(checked) => handleItemToggle('schedule', 'vatReport', checked)}
|
||||
disabled={!localSettings.schedule.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="종합소득세 신고 알림"
|
||||
checked={localSettings.schedule.incomeTaxReport}
|
||||
onChange={(checked) => handleItemToggle('schedule', 'incomeTaxReport', checked)}
|
||||
disabled={!localSettings.schedule.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 거래처 알림 */}
|
||||
<CategorySection
|
||||
title="거래처 알림"
|
||||
enabled={localSettings.vendor.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('vendor', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="신규 업체 등록 알림"
|
||||
checked={localSettings.vendor.newVendor}
|
||||
onChange={(checked) => handleItemToggle('vendor', 'newVendor', checked)}
|
||||
disabled={!localSettings.vendor.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="신용등급 알림"
|
||||
checked={localSettings.vendor.creditRating}
|
||||
onChange={(checked) => handleItemToggle('vendor', 'creditRating', checked)}
|
||||
disabled={!localSettings.vendor.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 근태 알림 */}
|
||||
<CategorySection
|
||||
title="근태 알림"
|
||||
enabled={localSettings.attendance.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('attendance', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="연차 알림"
|
||||
checked={localSettings.attendance.annualLeave}
|
||||
onChange={(checked) => handleItemToggle('attendance', 'annualLeave', checked)}
|
||||
disabled={!localSettings.attendance.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="출근 알림"
|
||||
checked={localSettings.attendance.clockIn}
|
||||
onChange={(checked) => handleItemToggle('attendance', 'clockIn', checked)}
|
||||
disabled={!localSettings.attendance.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="지각 알림"
|
||||
checked={localSettings.attendance.late}
|
||||
onChange={(checked) => handleItemToggle('attendance', 'late', checked)}
|
||||
disabled={!localSettings.attendance.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="결근 알림"
|
||||
checked={localSettings.attendance.absent}
|
||||
onChange={(checked) => handleItemToggle('attendance', 'absent', checked)}
|
||||
disabled={!localSettings.attendance.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 수주/발주 알림 */}
|
||||
<CategorySection
|
||||
title="수주/발주 알림"
|
||||
enabled={localSettings.order.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('order', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="수주 알림"
|
||||
checked={localSettings.order.salesOrder}
|
||||
onChange={(checked) => handleItemToggle('order', 'salesOrder', checked)}
|
||||
disabled={!localSettings.order.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="발주 알림"
|
||||
checked={localSettings.order.purchaseOrder}
|
||||
onChange={(checked) => handleItemToggle('order', 'purchaseOrder', checked)}
|
||||
disabled={!localSettings.order.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 전자결재 알림 */}
|
||||
<CategorySection
|
||||
title="전자결재 알림"
|
||||
enabled={localSettings.approval.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('approval', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="결재요청 알림"
|
||||
checked={localSettings.approval.approvalRequest}
|
||||
onChange={(checked) => handleItemToggle('approval', 'approvalRequest', checked)}
|
||||
disabled={!localSettings.approval.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="기안 > 승인 알림"
|
||||
checked={localSettings.approval.draftApproved}
|
||||
onChange={(checked) => handleItemToggle('approval', 'draftApproved', checked)}
|
||||
disabled={!localSettings.approval.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="기안 > 반려 알림"
|
||||
checked={localSettings.approval.draftRejected}
|
||||
onChange={(checked) => handleItemToggle('approval', 'draftRejected', checked)}
|
||||
disabled={!localSettings.approval.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="기안 > 완료 알림"
|
||||
checked={localSettings.approval.draftCompleted}
|
||||
onChange={(checked) => handleItemToggle('approval', 'draftCompleted', checked)}
|
||||
disabled={!localSettings.approval.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
|
||||
{/* 생산 알림 */}
|
||||
<CategorySection
|
||||
title="생산 알림"
|
||||
enabled={localSettings.production.enabled}
|
||||
onEnabledChange={(enabled) => handleCategoryToggle('production', enabled)}
|
||||
>
|
||||
<ItemRow
|
||||
label="안전재고 알림"
|
||||
checked={localSettings.production.safetyStock}
|
||||
onChange={(checked) => handleItemToggle('production', 'safetyStock', checked)}
|
||||
disabled={!localSettings.production.enabled}
|
||||
/>
|
||||
<ItemRow
|
||||
label="생산완료 알림"
|
||||
checked={localSettings.production.productionComplete}
|
||||
onChange={(checked) => handleItemToggle('production', 'productionComplete', checked)}
|
||||
disabled={!localSettings.production.enabled}
|
||||
/>
|
||||
</CategorySection>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="sticky bottom-0 bg-gray-900 px-4 py-3 border-t border-gray-700 flex justify-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="bg-gray-700 border-gray-600 text-white hover:bg-gray-600 min-w-[80px]"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-gray-700 text-white hover:bg-gray-600 min-w-[80px]"
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
* 알림설정 페이지
|
||||
*
|
||||
* 각 알림 유형별로 ON/OFF 토글, 알림 소리 선택, 이메일 알림 체크박스를 제공합니다.
|
||||
* 항목 설정 기능으로 표시할 알림 카테고리/항목을 선택할 수 있습니다.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Bell, Save, Play } from 'lucide-react';
|
||||
import { Bell, Save, Play, Settings } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -22,9 +23,10 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { toast } from 'sonner';
|
||||
import type { NotificationSettings, NotificationItem, SoundType } from './types';
|
||||
import { SOUND_OPTIONS } from './types';
|
||||
import type { NotificationSettings, NotificationItem, SoundType, ItemVisibilitySettings } from './types';
|
||||
import { SOUND_OPTIONS, DEFAULT_ITEM_VISIBILITY } from './types';
|
||||
import { saveNotificationSettings } from './actions';
|
||||
import { ItemSettingsDialog } from './ItemSettingsDialog';
|
||||
|
||||
// 미리듣기 함수
|
||||
function playPreviewSound(soundType: SoundType) {
|
||||
@@ -153,9 +155,28 @@ interface NotificationSettingsManagementProps {
|
||||
initialData: NotificationSettings;
|
||||
}
|
||||
|
||||
const ITEM_VISIBILITY_STORAGE_KEY = 'notification-item-visibility';
|
||||
|
||||
export function NotificationSettingsManagement({ initialData }: NotificationSettingsManagementProps) {
|
||||
const [settings, setSettings] = useState<NotificationSettings>(initialData);
|
||||
|
||||
// 항목 설정 (표시/숨김)
|
||||
const [itemVisibility, setItemVisibility] = useState<ItemVisibilitySettings>(() => {
|
||||
if (typeof window === 'undefined') return DEFAULT_ITEM_VISIBILITY;
|
||||
const saved = localStorage.getItem(ITEM_VISIBILITY_STORAGE_KEY);
|
||||
return saved ? JSON.parse(saved) : DEFAULT_ITEM_VISIBILITY;
|
||||
});
|
||||
|
||||
// 항목 설정 모달 상태
|
||||
const [isItemSettingsOpen, setIsItemSettingsOpen] = useState(false);
|
||||
|
||||
// 항목 설정 저장
|
||||
const handleItemVisibilitySave = useCallback((newSettings: ItemVisibilitySettings) => {
|
||||
setItemVisibility(newSettings);
|
||||
localStorage.setItem(ITEM_VISIBILITY_STORAGE_KEY, JSON.stringify(newSettings));
|
||||
toast.success('항목 설정이 저장되었습니다.');
|
||||
}, []);
|
||||
|
||||
// 공지 알림 핸들러
|
||||
const handleNoticeEnabledChange = (enabled: boolean) => {
|
||||
setSettings(prev => ({
|
||||
@@ -342,185 +363,245 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
|
||||
icon={Bell}
|
||||
/>
|
||||
|
||||
{/* 상단 버튼 영역 */}
|
||||
<div className="flex justify-end gap-2 mb-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsItemSettingsOpen(true)}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white border-orange-500"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
항목 설정
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 공지 알림 */}
|
||||
<NotificationSection
|
||||
title="공지 알림"
|
||||
enabled={settings.notice.enabled}
|
||||
onEnabledChange={handleNoticeEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="공지사항 알림"
|
||||
item={settings.notice.notice}
|
||||
onChange={(item) => handleNoticeItemChange('notice', item)}
|
||||
disabled={!settings.notice.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="이벤트 알림"
|
||||
item={settings.notice.event}
|
||||
onChange={(item) => handleNoticeItemChange('event', item)}
|
||||
disabled={!settings.notice.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.notice.enabled && (
|
||||
<NotificationSection
|
||||
title="공지 알림"
|
||||
enabled={settings.notice.enabled}
|
||||
onEnabledChange={handleNoticeEnabledChange}
|
||||
>
|
||||
{itemVisibility.notice.notice && (
|
||||
<NotificationItemRow
|
||||
label="공지사항 알림"
|
||||
item={settings.notice.notice}
|
||||
onChange={(item) => handleNoticeItemChange('notice', item)}
|
||||
disabled={!settings.notice.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.notice.event && (
|
||||
<NotificationItemRow
|
||||
label="이벤트 알림"
|
||||
item={settings.notice.event}
|
||||
onChange={(item) => handleNoticeItemChange('event', item)}
|
||||
disabled={!settings.notice.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 일정 알림 */}
|
||||
<NotificationSection
|
||||
title="일정 알림"
|
||||
enabled={settings.schedule.enabled}
|
||||
onEnabledChange={handleScheduleEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="부가세 신고 알림"
|
||||
item={settings.schedule.vatReport}
|
||||
onChange={(item) => handleScheduleItemChange('vatReport', item)}
|
||||
disabled={!settings.schedule.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="종합소득세 신고 알림"
|
||||
item={settings.schedule.incomeTaxReport}
|
||||
onChange={(item) => handleScheduleItemChange('incomeTaxReport', item)}
|
||||
disabled={!settings.schedule.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.schedule.enabled && (
|
||||
<NotificationSection
|
||||
title="일정 알림"
|
||||
enabled={settings.schedule.enabled}
|
||||
onEnabledChange={handleScheduleEnabledChange}
|
||||
>
|
||||
{itemVisibility.schedule.vatReport && (
|
||||
<NotificationItemRow
|
||||
label="부가세 신고 알림"
|
||||
item={settings.schedule.vatReport}
|
||||
onChange={(item) => handleScheduleItemChange('vatReport', item)}
|
||||
disabled={!settings.schedule.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.schedule.incomeTaxReport && (
|
||||
<NotificationItemRow
|
||||
label="종합소득세 신고 알림"
|
||||
item={settings.schedule.incomeTaxReport}
|
||||
onChange={(item) => handleScheduleItemChange('incomeTaxReport', item)}
|
||||
disabled={!settings.schedule.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 거래처 알림 */}
|
||||
<NotificationSection
|
||||
title="거래처 알림"
|
||||
enabled={settings.vendor.enabled}
|
||||
onEnabledChange={handleVendorEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="신규 업체 등록 알림"
|
||||
item={settings.vendor.newVendor}
|
||||
onChange={(item) => handleVendorItemChange('newVendor', item)}
|
||||
disabled={!settings.vendor.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="신용등급 등록 알림"
|
||||
item={settings.vendor.creditRating}
|
||||
onChange={(item) => handleVendorItemChange('creditRating', item)}
|
||||
disabled={!settings.vendor.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.vendor.enabled && (
|
||||
<NotificationSection
|
||||
title="거래처 알림"
|
||||
enabled={settings.vendor.enabled}
|
||||
onEnabledChange={handleVendorEnabledChange}
|
||||
>
|
||||
{itemVisibility.vendor.newVendor && (
|
||||
<NotificationItemRow
|
||||
label="신규 업체 등록 알림"
|
||||
item={settings.vendor.newVendor}
|
||||
onChange={(item) => handleVendorItemChange('newVendor', item)}
|
||||
disabled={!settings.vendor.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.vendor.creditRating && (
|
||||
<NotificationItemRow
|
||||
label="신용등급 등록 알림"
|
||||
item={settings.vendor.creditRating}
|
||||
onChange={(item) => handleVendorItemChange('creditRating', item)}
|
||||
disabled={!settings.vendor.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 근태 알림 */}
|
||||
<NotificationSection
|
||||
title="근태 알림"
|
||||
enabled={settings.attendance.enabled}
|
||||
onEnabledChange={handleAttendanceEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="연차 알림"
|
||||
item={settings.attendance.annualLeave}
|
||||
onChange={(item) => handleAttendanceItemChange('annualLeave', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="출근 알림"
|
||||
item={settings.attendance.clockIn}
|
||||
onChange={(item) => handleAttendanceItemChange('clockIn', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="지각 알림"
|
||||
item={settings.attendance.late}
|
||||
onChange={(item) => handleAttendanceItemChange('late', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="결근 알림"
|
||||
item={settings.attendance.absent}
|
||||
onChange={(item) => handleAttendanceItemChange('absent', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.attendance.enabled && (
|
||||
<NotificationSection
|
||||
title="근태 알림"
|
||||
enabled={settings.attendance.enabled}
|
||||
onEnabledChange={handleAttendanceEnabledChange}
|
||||
>
|
||||
{itemVisibility.attendance.annualLeave && (
|
||||
<NotificationItemRow
|
||||
label="연차 알림"
|
||||
item={settings.attendance.annualLeave}
|
||||
onChange={(item) => handleAttendanceItemChange('annualLeave', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.attendance.clockIn && (
|
||||
<NotificationItemRow
|
||||
label="출근 알림"
|
||||
item={settings.attendance.clockIn}
|
||||
onChange={(item) => handleAttendanceItemChange('clockIn', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.attendance.late && (
|
||||
<NotificationItemRow
|
||||
label="지각 알림"
|
||||
item={settings.attendance.late}
|
||||
onChange={(item) => handleAttendanceItemChange('late', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.attendance.absent && (
|
||||
<NotificationItemRow
|
||||
label="결근 알림"
|
||||
item={settings.attendance.absent}
|
||||
onChange={(item) => handleAttendanceItemChange('absent', item)}
|
||||
disabled={!settings.attendance.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 수주/발주 알림 */}
|
||||
<NotificationSection
|
||||
title="수주/발주 알림"
|
||||
enabled={settings.order.enabled}
|
||||
onEnabledChange={handleOrderEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="수주 등록 알림"
|
||||
item={settings.order.salesOrder}
|
||||
onChange={(item) => handleOrderItemChange('salesOrder', item)}
|
||||
disabled={!settings.order.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="발주 알림"
|
||||
item={settings.order.purchaseOrder}
|
||||
onChange={(item) => handleOrderItemChange('purchaseOrder', item)}
|
||||
disabled={!settings.order.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="결재요청 알림"
|
||||
item={settings.order.approvalRequest}
|
||||
onChange={(item) => handleOrderItemChange('approvalRequest', item)}
|
||||
disabled={!settings.order.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.order.enabled && (
|
||||
<NotificationSection
|
||||
title="수주/발주 알림"
|
||||
enabled={settings.order.enabled}
|
||||
onEnabledChange={handleOrderEnabledChange}
|
||||
>
|
||||
{itemVisibility.order.salesOrder && (
|
||||
<NotificationItemRow
|
||||
label="수주 등록 알림"
|
||||
item={settings.order.salesOrder}
|
||||
onChange={(item) => handleOrderItemChange('salesOrder', item)}
|
||||
disabled={!settings.order.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.order.purchaseOrder && (
|
||||
<NotificationItemRow
|
||||
label="발주 알림"
|
||||
item={settings.order.purchaseOrder}
|
||||
onChange={(item) => handleOrderItemChange('purchaseOrder', item)}
|
||||
disabled={!settings.order.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 전자결재 알림 */}
|
||||
<NotificationSection
|
||||
title="전자결재 알림"
|
||||
enabled={settings.approval.enabled}
|
||||
onEnabledChange={handleApprovalEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="결재요청 알림"
|
||||
item={settings.approval.approvalRequest}
|
||||
onChange={(item) => handleApprovalItemChange('approvalRequest', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="기안 > 승인 알림"
|
||||
item={settings.approval.draftApproved}
|
||||
onChange={(item) => handleApprovalItemChange('draftApproved', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="기안 > 반려 알림"
|
||||
item={settings.approval.draftRejected}
|
||||
onChange={(item) => handleApprovalItemChange('draftRejected', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="기안 > 완료 알림"
|
||||
item={settings.approval.draftCompleted}
|
||||
onChange={(item) => handleApprovalItemChange('draftCompleted', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
{itemVisibility.approval.enabled && (
|
||||
<NotificationSection
|
||||
title="전자결재 알림"
|
||||
enabled={settings.approval.enabled}
|
||||
onEnabledChange={handleApprovalEnabledChange}
|
||||
>
|
||||
{itemVisibility.approval.approvalRequest && (
|
||||
<NotificationItemRow
|
||||
label="결재요청 알림"
|
||||
item={settings.approval.approvalRequest}
|
||||
onChange={(item) => handleApprovalItemChange('approvalRequest', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.approval.draftApproved && (
|
||||
<NotificationItemRow
|
||||
label="기안 > 승인 알림"
|
||||
item={settings.approval.draftApproved}
|
||||
onChange={(item) => handleApprovalItemChange('draftApproved', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.approval.draftRejected && (
|
||||
<NotificationItemRow
|
||||
label="기안 > 반려 알림"
|
||||
item={settings.approval.draftRejected}
|
||||
onChange={(item) => handleApprovalItemChange('draftRejected', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.approval.draftCompleted && (
|
||||
<NotificationItemRow
|
||||
label="기안 > 완료 알림"
|
||||
item={settings.approval.draftCompleted}
|
||||
onChange={(item) => handleApprovalItemChange('draftCompleted', item)}
|
||||
disabled={!settings.approval.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
|
||||
{/* 생산 알림 */}
|
||||
<NotificationSection
|
||||
title="생산 알림"
|
||||
enabled={settings.production.enabled}
|
||||
onEnabledChange={handleProductionEnabledChange}
|
||||
>
|
||||
<NotificationItemRow
|
||||
label="안전재고 알림"
|
||||
item={settings.production.safetyStock}
|
||||
onChange={(item) => handleProductionItemChange('safetyStock', item)}
|
||||
disabled={!settings.production.enabled}
|
||||
/>
|
||||
<NotificationItemRow
|
||||
label="생산완료 알림"
|
||||
item={settings.production.productionComplete}
|
||||
onChange={(item) => handleProductionItemChange('productionComplete', item)}
|
||||
disabled={!settings.production.enabled}
|
||||
/>
|
||||
</NotificationSection>
|
||||
|
||||
{/* 저장 버튼 */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={handleSave} size="lg">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
{itemVisibility.production.enabled && (
|
||||
<NotificationSection
|
||||
title="생산 알림"
|
||||
enabled={settings.production.enabled}
|
||||
onEnabledChange={handleProductionEnabledChange}
|
||||
>
|
||||
{itemVisibility.production.safetyStock && (
|
||||
<NotificationItemRow
|
||||
label="안전재고 알림"
|
||||
item={settings.production.safetyStock}
|
||||
onChange={(item) => handleProductionItemChange('safetyStock', item)}
|
||||
disabled={!settings.production.enabled}
|
||||
/>
|
||||
)}
|
||||
{itemVisibility.production.productionComplete && (
|
||||
<NotificationItemRow
|
||||
label="생산완료 알림"
|
||||
item={settings.production.productionComplete}
|
||||
onChange={(item) => handleProductionItemChange('productionComplete', item)}
|
||||
disabled={!settings.production.enabled}
|
||||
/>
|
||||
)}
|
||||
</NotificationSection>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 항목 설정 모달 */}
|
||||
<ItemSettingsDialog
|
||||
isOpen={isItemSettingsOpen}
|
||||
onClose={() => setIsItemSettingsOpen(false)}
|
||||
settings={itemVisibility}
|
||||
onSave={handleItemVisibilitySave}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -112,6 +112,72 @@ export interface NotificationSettings {
|
||||
production: ProductionNotificationSettings;
|
||||
}
|
||||
|
||||
// ===== 항목 설정 (표시/숨김) 타입 =====
|
||||
|
||||
// 공지 알림 항목 설정
|
||||
export interface NoticeItemVisibility {
|
||||
enabled: boolean;
|
||||
notice: boolean; // 공지사항 알림
|
||||
event: boolean; // 이벤트 알림
|
||||
}
|
||||
|
||||
// 일정 알림 항목 설정
|
||||
export interface ScheduleItemVisibility {
|
||||
enabled: boolean;
|
||||
vatReport: boolean; // 부가세 신고 알림
|
||||
incomeTaxReport: boolean; // 종합소득세 신고 알림
|
||||
}
|
||||
|
||||
// 거래처 알림 항목 설정
|
||||
export interface VendorItemVisibility {
|
||||
enabled: boolean;
|
||||
newVendor: boolean; // 신규 업체 등록 알림
|
||||
creditRating: boolean; // 신용등급 알림
|
||||
}
|
||||
|
||||
// 근태 알림 항목 설정
|
||||
export interface AttendanceItemVisibility {
|
||||
enabled: boolean;
|
||||
annualLeave: boolean; // 연차 알림
|
||||
clockIn: boolean; // 출근 알림
|
||||
late: boolean; // 지각 알림
|
||||
absent: boolean; // 결근 알림
|
||||
}
|
||||
|
||||
// 수주/발주 알림 항목 설정
|
||||
export interface OrderItemVisibility {
|
||||
enabled: boolean;
|
||||
salesOrder: boolean; // 수주 알림
|
||||
purchaseOrder: boolean; // 발주 알림
|
||||
}
|
||||
|
||||
// 전자결재 알림 항목 설정
|
||||
export interface ApprovalItemVisibility {
|
||||
enabled: boolean;
|
||||
approvalRequest: boolean; // 결재요청 알림
|
||||
draftApproved: boolean; // 기안 > 승인 알림
|
||||
draftRejected: boolean; // 기안 > 반려 알림
|
||||
draftCompleted: boolean; // 기안 > 완료 알림
|
||||
}
|
||||
|
||||
// 생산 알림 항목 설정
|
||||
export interface ProductionItemVisibility {
|
||||
enabled: boolean;
|
||||
safetyStock: boolean; // 안전재고 알림
|
||||
productionComplete: boolean; // 생산완료 알림
|
||||
}
|
||||
|
||||
// 전체 항목 설정
|
||||
export interface ItemVisibilitySettings {
|
||||
notice: NoticeItemVisibility;
|
||||
schedule: ScheduleItemVisibility;
|
||||
vendor: VendorItemVisibility;
|
||||
attendance: AttendanceItemVisibility;
|
||||
order: OrderItemVisibility;
|
||||
approval: ApprovalItemVisibility;
|
||||
production: ProductionItemVisibility;
|
||||
}
|
||||
|
||||
// 기본값
|
||||
export const DEFAULT_NOTIFICATION_ITEM: NotificationItem = {
|
||||
enabled: false,
|
||||
@@ -160,4 +226,47 @@ export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
|
||||
safetyStock: { enabled: false, email: false, soundType: 'default' },
|
||||
productionComplete: { enabled: true, email: false, soundType: 'sam_voice' },
|
||||
},
|
||||
};
|
||||
|
||||
// 항목 설정 기본값 (모두 표시)
|
||||
export const DEFAULT_ITEM_VISIBILITY: ItemVisibilitySettings = {
|
||||
notice: {
|
||||
enabled: true,
|
||||
notice: true,
|
||||
event: true,
|
||||
},
|
||||
schedule: {
|
||||
enabled: true,
|
||||
vatReport: true,
|
||||
incomeTaxReport: true,
|
||||
},
|
||||
vendor: {
|
||||
enabled: true,
|
||||
newVendor: true,
|
||||
creditRating: true,
|
||||
},
|
||||
attendance: {
|
||||
enabled: true,
|
||||
annualLeave: true,
|
||||
clockIn: true,
|
||||
late: true,
|
||||
absent: true,
|
||||
},
|
||||
order: {
|
||||
enabled: true,
|
||||
salesOrder: true,
|
||||
purchaseOrder: true,
|
||||
},
|
||||
approval: {
|
||||
enabled: true,
|
||||
approvalRequest: true,
|
||||
draftApproved: true,
|
||||
draftRejected: true,
|
||||
draftCompleted: true,
|
||||
},
|
||||
production: {
|
||||
enabled: true,
|
||||
safetyStock: true,
|
||||
productionComplete: true,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user