feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
452
src/components/settings/NotificationSettings/index.tsx
Normal file
452
src/components/settings/NotificationSettings/index.tsx
Normal file
@@ -0,0 +1,452 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 알림설정 페이지
|
||||
*
|
||||
* 각 알림 유형별로 ON/OFF 토글과 이메일 알림 체크박스를 제공합니다.
|
||||
*
|
||||
* TODO: API 연동 시 작업 사항
|
||||
* - GET /api/settings/notifications: 설정값 조회
|
||||
* - PUT /api/settings/notifications: 설정값 저장
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Bell, Save } 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 { toast } from 'sonner';
|
||||
import type { NotificationSettings, NotificationItem } from './types';
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS } from './types';
|
||||
|
||||
// 알림 항목 컴포넌트
|
||||
interface NotificationItemRowProps {
|
||||
label: string;
|
||||
item: NotificationItem;
|
||||
onChange: (item: NotificationItem) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function NotificationItemRow({ label, item, onChange, disabled }: NotificationItemRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-3 border-b last:border-b-0">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<span className="text-sm min-w-[160px]">{label}</span>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={item.email}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...item, email: checked === true })
|
||||
}
|
||||
disabled={disabled || !item.enabled}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">이메일</span>
|
||||
</label>
|
||||
</div>
|
||||
<Switch
|
||||
checked={item.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...item, enabled: checked, email: checked ? item.email : false })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 알림 섹션 컴포넌트
|
||||
interface NotificationSectionProps {
|
||||
title: string;
|
||||
enabled: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function NotificationSection({ title, enabled, onEnabledChange, children }: NotificationSectionProps) {
|
||||
console.log(`[NotificationSection] ${title} enabled:`, enabled);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between px-6 pt-6 pb-3">
|
||||
<CardTitle className="text-base font-medium">{title}</CardTitle>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
console.log(`[Switch] ${title} clicked:`, checked);
|
||||
onEnabledChange(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="pt-0">
|
||||
<div className="pl-4">
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotificationSettingsManagement() {
|
||||
const [settings, setSettings] = useState<NotificationSettings>(DEFAULT_NOTIFICATION_SETTINGS);
|
||||
|
||||
// 공지 알림 핸들러
|
||||
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 },
|
||||
}),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleOrderItemChange = (key: 'salesOrder' | 'purchaseOrder', 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 = () => {
|
||||
// TODO: API 호출로 설정 저장
|
||||
console.log('저장할 알림 설정:', settings);
|
||||
toast.success('알림 설정이 저장되었습니다.');
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="알림설정"
|
||||
description="알림 설정을 관리합니다."
|
||||
icon={Bell}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 일정 알림 */}
|
||||
<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>
|
||||
|
||||
{/* 거래처 알림 */}
|
||||
<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>
|
||||
|
||||
{/* 근태 알림 */}
|
||||
<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>
|
||||
|
||||
{/* 수주/발주 알림 */}
|
||||
<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}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 생산 알림 */}
|
||||
<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>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
121
src/components/settings/NotificationSettings/types.ts
Normal file
121
src/components/settings/NotificationSettings/types.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 알림 설정 타입 정의
|
||||
*/
|
||||
|
||||
// 개별 알림 항목 설정
|
||||
export interface NotificationItem {
|
||||
enabled: boolean;
|
||||
email: boolean;
|
||||
}
|
||||
|
||||
// 공지 알림 설정
|
||||
export interface NoticeNotificationSettings {
|
||||
enabled: boolean; // 섹션 전체 토글
|
||||
notice: NotificationItem; // 공지사항 알림
|
||||
event: NotificationItem; // 이벤트 알림
|
||||
}
|
||||
|
||||
// 일정 알림 설정
|
||||
export interface ScheduleNotificationSettings {
|
||||
enabled: boolean;
|
||||
vatReport: NotificationItem; // 부가세 신고 알림
|
||||
incomeTaxReport: NotificationItem; // 종합소득세 신고 알림
|
||||
}
|
||||
|
||||
// 거래처 알림 설정
|
||||
export interface VendorNotificationSettings {
|
||||
enabled: boolean;
|
||||
newVendor: NotificationItem; // 신규 업체 등록 알림
|
||||
creditRating: NotificationItem; // 신용등급 등록 알림
|
||||
}
|
||||
|
||||
// 근태 알림 설정
|
||||
export interface AttendanceNotificationSettings {
|
||||
enabled: boolean;
|
||||
annualLeave: NotificationItem; // 연차 알림
|
||||
clockIn: NotificationItem; // 출근 알림
|
||||
late: NotificationItem; // 지각 알림
|
||||
absent: NotificationItem; // 결근 알림
|
||||
}
|
||||
|
||||
// 수주/발주 알림 설정
|
||||
export interface OrderNotificationSettings {
|
||||
enabled: boolean;
|
||||
salesOrder: NotificationItem; // 수주 등록 알림
|
||||
purchaseOrder: NotificationItem; // 발주 알림
|
||||
}
|
||||
|
||||
// 전자결재 알림 설정
|
||||
export interface ApprovalNotificationSettings {
|
||||
enabled: boolean;
|
||||
approvalRequest: NotificationItem; // 결재요청 알림
|
||||
draftApproved: NotificationItem; // 기안 > 승인 알림
|
||||
draftRejected: NotificationItem; // 기안 > 반려 알림
|
||||
draftCompleted: NotificationItem; // 기안 > 완료 알림
|
||||
}
|
||||
|
||||
// 생산 알림 설정
|
||||
export interface ProductionNotificationSettings {
|
||||
enabled: boolean;
|
||||
safetyStock: NotificationItem; // 안전재고 알림
|
||||
productionComplete: NotificationItem; // 생산완료 알림
|
||||
}
|
||||
|
||||
// 전체 알림 설정
|
||||
export interface NotificationSettings {
|
||||
notice: NoticeNotificationSettings;
|
||||
schedule: ScheduleNotificationSettings;
|
||||
vendor: VendorNotificationSettings;
|
||||
attendance: AttendanceNotificationSettings;
|
||||
order: OrderNotificationSettings;
|
||||
approval: ApprovalNotificationSettings;
|
||||
production: ProductionNotificationSettings;
|
||||
}
|
||||
|
||||
// 기본값
|
||||
export const DEFAULT_NOTIFICATION_ITEM: NotificationItem = {
|
||||
enabled: false,
|
||||
email: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
|
||||
notice: {
|
||||
enabled: false,
|
||||
notice: { enabled: false, email: false },
|
||||
event: { enabled: true, email: false },
|
||||
},
|
||||
schedule: {
|
||||
enabled: false,
|
||||
vatReport: { enabled: false, email: false },
|
||||
incomeTaxReport: { enabled: true, email: false },
|
||||
},
|
||||
vendor: {
|
||||
enabled: false,
|
||||
newVendor: { enabled: false, email: false },
|
||||
creditRating: { enabled: true, email: false },
|
||||
},
|
||||
attendance: {
|
||||
enabled: false,
|
||||
annualLeave: { enabled: false, email: false },
|
||||
clockIn: { enabled: true, email: false },
|
||||
late: { enabled: false, email: false },
|
||||
absent: { enabled: true, email: false },
|
||||
},
|
||||
order: {
|
||||
enabled: false,
|
||||
salesOrder: { enabled: false, email: false },
|
||||
purchaseOrder: { enabled: true, email: false },
|
||||
},
|
||||
approval: {
|
||||
enabled: false,
|
||||
approvalRequest: { enabled: false, email: false },
|
||||
draftApproved: { enabled: true, email: false },
|
||||
draftRejected: { enabled: false, email: false },
|
||||
draftCompleted: { enabled: true, email: false },
|
||||
},
|
||||
production: {
|
||||
enabled: false,
|
||||
safetyStock: { enabled: false, email: false },
|
||||
productionComplete: { enabled: true, email: false },
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user