'use client' import { useState, useTransition } from 'react' import { useToast } from '@/hooks/use-toast' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Save, Edit3, X, Check, Lock, Shield, Clock, Smartphone, RotateCcw, AlertTriangle } from 'lucide-react' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { resetSecuritySettings, updateSecuritySettings } from '@/lib/password-policy/service' export interface SecuritySettings { id: number // 패스워드 정책 minPasswordLength: number requireUppercase: boolean requireLowercase: boolean requireNumbers: boolean requireSymbols: boolean passwordExpiryDays: number | null passwordHistoryCount: number // 계정 잠금 정책 maxFailedAttempts: number lockoutDurationMinutes: number // MFA 정책 requireMfaForPartners: boolean smsTokenExpiryMinutes: number maxSmsAttemptsPerDay: number // 세션 관리 sessionTimeoutMinutes: number // 메타데이터 createdAt: Date updatedAt: Date } interface Props { initialSettings: SecuritySettings } interface SettingItem { key: keyof SecuritySettings label: string description: string type: 'number' | 'boolean' | 'nullable-number' min?: number max?: number unit?: string } const settingCategories = [ { title: '패스워드 정책', description: '사용자 비밀번호 요구사항을 설정합니다', icon: Lock, items: [ { key: 'minPasswordLength' as const, label: '최소 길이', description: '비밀번호 최소 문자 수', type: 'number' as const, min: 4, max: 128, unit: '자' }, { key: 'requireUppercase' as const, label: '대문자 필수', description: '대문자 포함 필수 여부', type: 'boolean' as const }, { key: 'requireLowercase' as const, label: '소문자 필수', description: '소문자 포함 필수 여부', type: 'boolean' as const }, { key: 'requireNumbers' as const, label: '숫자 필수', description: '숫자 포함 필수 여부', type: 'boolean' as const }, { key: 'requireSymbols' as const, label: '특수문자 필수', description: '특수문자 포함 필수 여부', type: 'boolean' as const }, { key: 'passwordExpiryDays' as const, label: '만료 기간', description: '비밀번호 만료일 (0이면 만료 없음)', type: 'nullable-number' as const, min: 0, max: 365, unit: '일' }, { key: 'passwordHistoryCount' as const, label: '히스토리 개수', description: '중복 방지할 이전 비밀번호 개수', type: 'number' as const, min: 0, max: 20, unit: '개' } ] }, { title: '계정 잠금 정책', description: '로그인 실패 시 계정 잠금 설정', icon: Shield, items: [ { key: 'maxFailedAttempts' as const, label: '최대 실패 횟수', description: '계정 잠금까지 허용되는 로그인 실패 횟수', type: 'number' as const, min: 1, max: 20, unit: '회' }, { key: 'lockoutDurationMinutes' as const, label: '잠금 시간', description: '계정 잠금 지속 시간', type: 'number' as const, min: 1, max: 1440, unit: '분' } ] }, { title: 'MFA 정책', description: '다단계 인증 설정', icon: Smartphone, items: [ { key: 'requireMfaForPartners' as const, label: '협력업체 MFA 필수', description: '협력업체 사용자 MFA 인증 필수 여부', type: 'boolean' as const }, { key: 'smsTokenExpiryMinutes' as const, label: 'SMS 토큰 만료', description: 'SMS 인증 토큰 만료 시간', type: 'number' as const, min: 1, max: 30, unit: '분' }, { key: 'maxSmsAttemptsPerDay' as const, label: '일일 SMS 한도', description: '일일 SMS 전송 최대 횟수', type: 'number' as const, min: 1, max: 100, unit: '회' } ] }, { title: '세션 관리', description: '사용자 세션 설정', icon: Clock, items: [ { key: 'sessionTimeoutMinutes' as const, label: '세션 타임아웃', description: '비활성 상태에서 자동 로그아웃까지의 시간', type: 'number' as const, min: 5, max: 1440, unit: '분' } ] } ] export default function SecuritySettingsTable({ initialSettings }: Props) { const [settings, setSettings] = useState(initialSettings) const [editingItems, setEditingItems] = useState>(new Set()) const [tempValues, setTempValues] = useState>({}) const [isPending, startTransition] = useTransition() const { toast } = useToast() const handleEdit = (key: string) => { setEditingItems(prev => new Set([...prev, key])) setTempValues(prev => ({ ...prev, [key]: settings[key as keyof SecuritySettings] })) } const handleCancel = (key: string) => { setEditingItems(prev => { const newSet = new Set(prev) newSet.delete(key) return newSet }) setTempValues(prev => { const newValues = { ...prev } delete newValues[key] return newValues }) } const handleSave = async (key: string) => { const value = tempValues[key] const item = settingCategories .flatMap(cat => cat.items) .find(item => item.key === key) // 클라이언트 사이드 유효성 검사 if (item && item.type === 'number') { if (value < (item.min || 0) || value > (item.max || Infinity)) { toast({ title: '유효하지 않은 값', description: `${item.label}은(는) ${item.min || 0}${item.unit || ''} 이상 ${item.max || Infinity}${item.unit || ''} 이하여야 합니다.`, variant: 'destructive', }) return } } startTransition(async () => { try { const result = await updateSecuritySettings({ [key]: value }) if (result.success) { setSettings(prev => ({ ...prev, [key]: value, updatedAt: new Date() })) setEditingItems(prev => { const newSet = new Set(prev) newSet.delete(key) return newSet }) setTempValues(prev => { const newValues = { ...prev } delete newValues[key] return newValues }) toast({ title: '설정 저장됨', description: `${item?.label || '설정'}이(가) 성공적으로 업데이트되었습니다.`, }) } else { toast({ title: '저장 실패', description: result.error || '설정 저장 중 오류가 발생했습니다.', variant: 'destructive', }) } } catch (error) { toast({ title: '저장 실패', description: '설정 저장 중 오류가 발생했습니다.', variant: 'destructive', }) } }) } const handleValueChange = (key: string, value: any) => { setTempValues(prev => ({ ...prev, [key]: value })) } const handleReset = async () => { startTransition(async () => { try { const result = await resetSecuritySettings() if (result.success) { // 페이지 새로고침으로 최신 데이터 로드 window.location.reload() } else { toast({ title: '초기화 실패', description: result.error || '설정 초기화 중 오류가 발생했습니다.', variant: 'destructive', }) } } catch (error) { toast({ title: '초기화 실패', description: '설정 초기화 중 오류가 발생했습니다.', variant: 'destructive', }) } }) } const renderValue = (item: SettingItem) => { const key = item.key const isEditing = editingItems.has(key) const currentValue = isEditing ? tempValues[key] : settings[key] if (isEditing) { if (item.type === 'boolean') { return (
handleValueChange(key, checked)} /> {currentValue ? '사용' : '사용 안함'}
) } else { // nullable-number 타입을 위한 특별 처리 if (item.type === 'nullable-number') { return ( { const value = e.target.value === '' ? null : parseInt(e.target.value) handleValueChange(key, value) }} min={item.min} max={item.max} className="w-24" placeholder="0 (만료없음)" /> ) } else { return ( { const value = e.target.value === '' ? 0 : parseInt(e.target.value) handleValueChange(key, value) }} min={item.min} max={item.max} className="w-24" /> ) } } } // 읽기 모드 if (item.type === 'boolean') { return ( {currentValue ? '사용' : '사용 안함'} ) } else { let displayValue: string if (item.type === 'nullable-number') { if (currentValue === null || currentValue === 0) { displayValue = item.key === 'passwordExpiryDays' ? '만료 없음' : '사용 안함' } else { displayValue = `${currentValue}${item.unit || ''}` } } else { displayValue = `${currentValue || 0}${item.unit || ''}` } return ( {displayValue} ) } } const renderActions = (key: string) => { const isEditing = editingItems.has(key) if (isEditing) { return (
) } return ( ) } return (
{/* 액션 헤더 */}

각 항목을 클릭하여 수정할 수 있습니다.

설정 초기화 확인 모든 보안 설정을 기본값으로 초기화하시겠습니까?
이 작업은 되돌릴 수 없습니다.
취소 초기화
{settingCategories.map((category) => ( {category.title} {category.description} 설정 항목 설명 현재 값 작업 {category.items.map((item) => ( {item.label} {item.description} {renderValue(item)} {renderActions(item.key)} ))}
))}
마지막 업데이트: {settings.updatedAt.toLocaleString('ko-KR')} {editingItems.size > 0 && ( {editingItems.size}개 항목 편집 중 )}
) }