- 계정관리 상세/폼 개선 (AccountDetail, AccountDetailForm) - 근태설정, 휴가정책 관리 개선 - 바로빌 연동 회원가입 모달 개선 - 알림설정, 결제이력, 권한관리 UI 개선 - 직급/직책 관리 UI 개선 (RankManagement, TitleManagement) - 구독관리, 근무스케줄 관리 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
607 lines
20 KiB
TypeScript
607 lines
20 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 알림설정 페이지
|
|
*
|
|
* 각 알림 유형별로 ON/OFF 토글, 알림 소리 선택, 이메일 알림 체크박스를 제공합니다.
|
|
* 항목 설정 기능으로 표시할 알림 카테고리/항목을 선택할 수 있습니다.
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
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';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { toast } from 'sonner';
|
|
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) {
|
|
if (soundType === 'mute') {
|
|
toast.info('무음으로 설정되어 있습니다.');
|
|
return;
|
|
}
|
|
const soundName = soundType === 'default' ? '기본 알림음' : 'SAM 보이스';
|
|
toast.info(`${soundName} 미리듣기`);
|
|
}
|
|
|
|
// 알림 항목 컴포넌트
|
|
interface NotificationItemRowProps {
|
|
label: string;
|
|
item: NotificationItem;
|
|
onChange: (item: NotificationItem) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
function NotificationItemRow({ label, item, onChange, disabled }: NotificationItemRowProps) {
|
|
const isDisabled = disabled || !item.enabled;
|
|
|
|
return (
|
|
<div className="flex items-center justify-between py-4 border-b last:border-b-0">
|
|
<div className="flex-1 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<Switch
|
|
checked={item.enabled}
|
|
onCheckedChange={(checked) =>
|
|
onChange({
|
|
...item,
|
|
enabled: checked,
|
|
email: checked ? item.email : false
|
|
})
|
|
}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
|
|
{/* 알림 소리 선택 */}
|
|
<div className="space-y-1 pl-2">
|
|
<span className="text-xs text-muted-foreground">알림 소리 선택</span>
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={item.soundType}
|
|
onValueChange={(value: SoundType) =>
|
|
onChange({ ...item, soundType: value })
|
|
}
|
|
disabled={isDisabled}
|
|
>
|
|
<SelectTrigger className="flex-1 sm:w-[140px] sm:flex-none h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{SOUND_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0"
|
|
onClick={() => playPreviewSound(item.soundType)}
|
|
disabled={isDisabled}
|
|
>
|
|
<Play className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 추가 알림 선택 */}
|
|
<div className="space-y-1 pl-2">
|
|
<span className="text-xs text-muted-foreground">추가 알림 선택</span>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={item.email}
|
|
onCheckedChange={(checked) =>
|
|
onChange({ ...item, email: checked === true })
|
|
}
|
|
disabled={isDisabled}
|
|
/>
|
|
<span className="text-sm text-muted-foreground">이메일</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 알림 섹션 컴포넌트
|
|
interface NotificationSectionProps {
|
|
title: string;
|
|
enabled: boolean;
|
|
onEnabledChange: (enabled: boolean) => void;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function NotificationSection({ title, enabled, onEnabledChange, children }: NotificationSectionProps) {
|
|
|
|
return (
|
|
<Card>
|
|
<div className="flex items-center justify-between px-4 sm:px-6 pt-4 sm:pt-6 pb-3">
|
|
<CardTitle className="text-base font-medium">{title}</CardTitle>
|
|
<Switch
|
|
checked={enabled}
|
|
onCheckedChange={(checked) => {
|
|
onEnabledChange(checked);
|
|
}}
|
|
/>
|
|
</div>
|
|
<CardContent className="pt-0 px-4 sm:px-6">
|
|
<div className="pl-0 sm:pl-4">
|
|
{children}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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 => ({
|
|
...prev,
|
|
notice: {
|
|
...prev.notice,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
notice: { ...prev.notice.notice, enabled: false, email: false },
|
|
event: { ...prev.notice.event, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleNoticeItemChange = (key: 'notice' | 'event', item: NotificationItem) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
notice: { ...prev.notice, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 일정 알림 핸들러
|
|
const handleScheduleEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
schedule: {
|
|
...prev.schedule,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
vatReport: { ...prev.schedule.vatReport, enabled: false, email: false },
|
|
incomeTaxReport: { ...prev.schedule.incomeTaxReport, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleScheduleItemChange = (key: 'vatReport' | 'incomeTaxReport', item: NotificationItem) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
schedule: { ...prev.schedule, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 거래처 알림 핸들러
|
|
const handleVendorEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
vendor: {
|
|
...prev.vendor,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
newVendor: { ...prev.vendor.newVendor, enabled: false, email: false },
|
|
creditRating: { ...prev.vendor.creditRating, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleVendorItemChange = (key: 'newVendor' | 'creditRating', item: NotificationItem) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
vendor: { ...prev.vendor, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 근태 알림 핸들러
|
|
const handleAttendanceEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
attendance: {
|
|
...prev.attendance,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
annualLeave: { ...prev.attendance.annualLeave, enabled: false, email: false },
|
|
clockIn: { ...prev.attendance.clockIn, enabled: false, email: false },
|
|
late: { ...prev.attendance.late, enabled: false, email: false },
|
|
absent: { ...prev.attendance.absent, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleAttendanceItemChange = (
|
|
key: 'annualLeave' | 'clockIn' | 'late' | 'absent',
|
|
item: NotificationItem
|
|
) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
attendance: { ...prev.attendance, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 수주/발주 알림 핸들러
|
|
const handleOrderEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
order: {
|
|
...prev.order,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
salesOrder: { ...prev.order.salesOrder, enabled: false, email: false },
|
|
purchaseOrder: { ...prev.order.purchaseOrder, enabled: false, email: false },
|
|
approvalRequest: { ...prev.order.approvalRequest, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleOrderItemChange = (key: 'salesOrder' | 'purchaseOrder' | 'approvalRequest', item: NotificationItem) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
order: { ...prev.order, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 전자결재 알림 핸들러
|
|
const handleApprovalEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
approval: {
|
|
...prev.approval,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
approvalRequest: { ...prev.approval.approvalRequest, enabled: false, email: false },
|
|
draftApproved: { ...prev.approval.draftApproved, enabled: false, email: false },
|
|
draftRejected: { ...prev.approval.draftRejected, enabled: false, email: false },
|
|
draftCompleted: { ...prev.approval.draftCompleted, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleApprovalItemChange = (
|
|
key: 'approvalRequest' | 'draftApproved' | 'draftRejected' | 'draftCompleted',
|
|
item: NotificationItem
|
|
) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
approval: { ...prev.approval, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 생산 알림 핸들러
|
|
const handleProductionEnabledChange = (enabled: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
production: {
|
|
...prev.production,
|
|
enabled,
|
|
...(enabled ? {} : {
|
|
safetyStock: { ...prev.production.safetyStock, enabled: false, email: false },
|
|
productionComplete: { ...prev.production.productionComplete, enabled: false, email: false },
|
|
}),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleProductionItemChange = (
|
|
key: 'safetyStock' | 'productionComplete',
|
|
item: NotificationItem
|
|
) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
production: { ...prev.production, [key]: item },
|
|
}));
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
const result = await saveNotificationSettings(settings);
|
|
if (result.success) {
|
|
toast.success('알림 설정이 저장되었습니다.');
|
|
} else {
|
|
toast.error(result.error || '알림 설정 저장에 실패했습니다.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PageLayout>
|
|
<PageHeader
|
|
title="알림설정"
|
|
description="알림 설정을 관리합니다."
|
|
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">
|
|
{/* 공지 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 일정 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 거래처 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 근태 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 수주/발주 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 전자결재 알림 */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 생산 알림 */}
|
|
{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>
|
|
);
|
|
} |