From de2ac5a2860bc25180971e7a11f852d9d44675b7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 6 Aug 2025 04:23:40 +0000 Subject: (대표님) 정기평가, 법적검토, 정책, 가입관련 처리 및 관련 컴포넌트 추가, 메뉴 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/polices/policy-editor.tsx | 262 +++++++++++++++ components/polices/policy-history.tsx | 250 ++++++++++++++ components/polices/policy-management-client.tsx | 429 ++++++++++++++++++++++++ components/polices/policy-preview.tsx | 191 +++++++++++ 4 files changed, 1132 insertions(+) create mode 100644 components/polices/policy-editor.tsx create mode 100644 components/polices/policy-history.tsx create mode 100644 components/polices/policy-management-client.tsx create mode 100644 components/polices/policy-preview.tsx (limited to 'components/polices') diff --git a/components/polices/policy-editor.tsx b/components/polices/policy-editor.tsx new file mode 100644 index 00000000..d58831e0 --- /dev/null +++ b/components/polices/policy-editor.tsx @@ -0,0 +1,262 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Separator } from '@/components/ui/separator' +import { Save, Eye, X, Info, AlertTriangle } from 'lucide-react' +import TiptapEditor from '../qna/tiptap-editor' + +interface PolicyEditorProps { + policyType: string + currentPolicy?: any + onSave: (policyType: string, version: string, content: string) => void + onCancel: () => void + onPreview: (policyType: string, content: string, version: string) => void + isLoading?: boolean +} + +export function PolicyEditor({ + policyType, + currentPolicy, + onSave, + onCancel, + onPreview, + isLoading = false +}: PolicyEditorProps) { + const [version, setVersion] = useState('') + const [content, setContent] = useState('') + const [validationErrors, setValidationErrors] = useState([]) + + console.log(content) + + const policyLabels = { + privacy_policy: '개인정보 처리방침', + terms_of_service: '이용약관' + } + + // 현재 정책 기반으로 다음 버전 생성 + useEffect(() => { + if (currentPolicy) { + const currentVersion = currentPolicy.version + const versionParts = currentVersion.split('.') + const majorVersion = parseInt(versionParts[0]) || 1 + const minorVersion = parseInt(versionParts[1]) || 0 + + // 마이너 버전 업 + const nextVersion = `${majorVersion}.${minorVersion + 1}` + setVersion(nextVersion) + setContent(currentPolicy.content || '') + } else { + setVersion('1.0') + setContent(getDefaultPolicyContent(policyType)) + } + }, [currentPolicy, policyType]) + + const getDefaultPolicyContent = (type: string) => { + if (type === 'privacy_policy') { + return `

개인정보 처리방침

+ +

제1조 (목적)

+

본 개인정보 처리방침은 eVCP(이하 "회사")가 개인정보 보호법 등 관련 법령에 따라 정보주체의 개인정보를 보호하고 이와 관련된 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같은 처리방침을 수립·공개합니다.

+ +

제2조 (개인정보의 수집 및 이용목적)

+

회사는 다음의 목적을 위하여 개인정보를 처리합니다:

+ + +

제3조 (개인정보의 수집항목)

+

필수항목:

+ + +

제4조 (개인정보의 보유 및 이용기간)

+

회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.

+ +

제5조 (정보주체의 권리)

+

정보주체는 회사에 대해 언제든지 다음 각 호의 개인정보 보호 관련 권리를 행사할 수 있습니다:

+` + } else { + return `

이용약관

+ +

제1조 (목적)

+

본 약관은 eVCP(이하 "회사")가 제공하는 서비스의 이용조건 및 절차, 회사와 회원 간의 권리, 의무 및 책임사항을 규정함을 목적으로 합니다.

+ +

제2조 (정의)

+ + +

제3조 (약관의 효력 및 변경)

+

본 약관은 서비스를 이용하고자 하는 모든 회원에 대하여 그 효력을 발생합니다.

+ +

제4조 (회원가입)

+

회원가입은 신청자가 본 약관의 내용에 대하여 동의를 한 다음 회원가입신청을 하고 회사가 이러한 신청에 대하여 승낙함으로써 체결됩니다.

+ +

제5조 (서비스의 제공)

+

회사는 회원에게 다음과 같은 서비스를 제공합니다:

+` + } + } + + const validateForm = () => { + const errors: string[] = [] + + if (!version.trim()) { + errors.push('버전을 입력해주세요.') + } else if (!/^\d+\.\d+$/.test(version.trim())) { + errors.push('버전은 "1.0" 형식으로 입력해주세요.') + } + + if (!content.trim()) { + errors.push('정책 내용을 입력해주세요.') + } else if (content.trim().length < 100) { + errors.push('정책 내용이 너무 짧습니다. (최소 100자)') + } + + // 버전 중복 체크 (현재 정책이 있는 경우) + if (currentPolicy && version === currentPolicy.version) { + errors.push('이미 존재하는 버전입니다. 다른 버전을 입력해주세요.') + } + + setValidationErrors(errors) + return errors.length === 0 + } + + const handleSave = () => { + if (validateForm()) { + onSave(policyType, version.trim(), content) + } + } + + const handlePreview = () => { + if (validateForm()) { + onPreview(policyType, content, version.trim()) + } + } + + return ( + + + + {currentPolicy ? '정책 편집' : '새 정책 생성'} - {policyLabels[policyType]} + + + {currentPolicy + ? `현재 버전 v${currentPolicy.version}을 기반으로 새 버전을 생성합니다.` + : `${policyLabels[policyType]}의 첫 번째 버전을 생성합니다.` + } + + + + {/* 경고 메시지 */} + + + + 새 버전을 저장하면 즉시 활성화되어 모든 사용자에게 적용됩니다. + 저장하기 전에 미리보기로 내용을 확인해주세요. + + + + {/* 유효성 검사 오류 */} + {validationErrors.length > 0 && ( + + + +
    + {validationErrors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+
+ )} + + {/* 버전 입력 */} +
+ + setVersion(e.target.value)} + disabled={isLoading} + className="w-32" + /> +

+ 형식: 주.부 (예: 1.0, 1.1, 2.0) +

+
+ + + + {/* 정책 내용 편집기 */} +
+ +
+ +
+

+ 리치 텍스트 편집기를 사용하여 정책 내용을 작성하세요. + 이미지, 표, 목록 등을 추가할 수 있습니다. +

+
+ + {/* 액션 버튼들 */} +
+ + +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/polices/policy-history.tsx b/components/polices/policy-history.tsx new file mode 100644 index 00000000..af6a68f2 --- /dev/null +++ b/components/polices/policy-history.tsx @@ -0,0 +1,250 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Separator } from '@/components/ui/separator' +import { Save, Eye, X, Info, AlertTriangle, Calendar, Clock } from 'lucide-react' +import { Badge } from '../ui/badge' +import { PolicyPreview } from './policy-preview' + +// ✅ 타입 정의 +interface PolicyData { + id: number + policyType: 'privacy_policy' | 'terms_of_service' + version: string + content: string + effectiveDate: string + isCurrent: boolean + createdAt: string +} + +interface PolicyHistoryProps { + policyType: 'privacy_policy' | 'terms_of_service' + policies: PolicyData[] | null | undefined // ✅ null/undefined 허용 + currentPolicy?: PolicyData | null + onActivate: (policyId: number, policyType: string) => void + onEdit: () => void + onClose: () => void + isLoading?: boolean +} + +export function PolicyHistory({ + policyType, + policies, + currentPolicy, + onActivate, + onEdit, + onClose, + isLoading = false +}: PolicyHistoryProps) { + const [viewingPolicy, setViewingPolicy] = useState(null) // ✅ 상세보기 상태 + + const policyLabels: Record = { + privacy_policy: '개인정보 처리방침', + terms_of_service: '이용약관' + } + + // ✅ 디버깅 로그 + console.log('PolicyHistory - policies:', policies, 'type:', typeof policies, 'isArray:', Array.isArray(policies)) + + // ✅ 안전한 배열 변환 + const safePolicies = Array.isArray(policies) ? policies :Array.isArray(policies.data) ? policies.data : [] + + // ✅ 내용 미리보기 함수 + const getContentPreview = (content: string): string => { + if (!content) return '내용 없음' + + // HTML 태그 제거 및 텍스트 추출 + const textContent = content.replace(/<[^>]*>/g, '').trim() + return textContent.length > 200 + ? textContent.substring(0, 200) + '...' + : textContent + } + + // ✅ 상세보기 핸들러 + const handleViewDetail = (policy: PolicyData) => { + setViewingPolicy(policy) + } + + // ✅ 상세보기 닫기 핸들러 + const handleCloseDetail = () => { + setViewingPolicy(null) + } + + // ✅ 상세보기 모드일 때 PolicyPreview 렌더링 + if (viewingPolicy) { + return ( + + ) + } + + return ( + + +
+
+ + + 정책 히스토리 + + + {policyLabels[policyType]}의 모든 버전을 확인하고 관리합니다 + +
+
+ + +
+
+
+ + {/* ✅ 로딩 상태 */} + {isLoading && ( +
+
히스토리를 불러오는 중...
+
+ )} + + {/* ✅ 에러 상태 체크 */} + {!isLoading && safePolicies && !Array.isArray(safePolicies) && ( + + + + 정책 데이터 형식이 올바르지 않습니다. (받은 데이터: {typeof safePolicies}) + + + )} + + {/* ✅ 정책 목록 */} + {!isLoading && ( +
+ {safePolicies.length === 0 ? ( +
+
+ +

등록된 정책 버전이 없습니다.

+
+ +
+ ) : ( + <> + {/* ✅ 정책 개수 표시 */} +
+
+ 총 {safePolicies.length}개 버전 +
+ {currentPolicy && ( + + 현재: v{currentPolicy.version} + + )} +
+ + {/* ✅ 정책 목록 렌더링 */} + {safePolicies.map((policy: PolicyData) => ( +
+
+
+ {/* ✅ 헤더 */} +
+

+ 버전 {policy.version} +

+ {policy.isCurrent && ( + + 현재 활성 + + )} +
+ + {/* ✅ 메타 정보 */} +
+
+ + 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} +
+
+ + 생성일: {new Date(policy.createdAt).toLocaleDateString('ko-KR')} +
+
+ + {/* ✅ 내용 미리보기 */} +
+
+ {getContentPreview(policy.content)} +
+
+
+ + {/* ✅ 액션 버튼들 */} +
+ {!policy.isCurrent && ( + + )} + +
+
+
+ ))} + + )} +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/components/polices/policy-management-client.tsx b/components/polices/policy-management-client.tsx new file mode 100644 index 00000000..eecb82ff --- /dev/null +++ b/components/polices/policy-management-client.tsx @@ -0,0 +1,429 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { + Plus, + Edit, + Eye, + History, + Save, + X, + Shield, + FileText, + Calendar, + Clock, + CheckCircle2, + AlertCircle, + Loader2 +} from 'lucide-react' +import { PolicyEditor } from './policy-editor' +import { PolicyPreview } from './policy-preview' +import { PolicyHistory } from './policy-history' +import { useToast } from "@/hooks/use-toast"; +import { activatePolicyVersion, createPolicyVersion, getPolicyHistory } from '@/lib/polices/service' +import { useRouter } from "next/navigation" + +interface PolicyManagementClientProps { + initialData: { + currentPolicies: Record + allPolicies: Record + stats: any + } +} + +export function PolicyManagementClient({ initialData }: PolicyManagementClientProps) { + const [currentTab, setCurrentTab] = useState('privacy_policy') + const [editingPolicy, setEditingPolicy] = useState(null) + const [viewingHistory, setViewingHistory] = useState(null) + const [previewData, setPreviewData] = useState(null) + + // ✅ 초기 데이터를 안전하게 설정 + const [policies, setPolicies] = useState(() => { + const safePolicies = { ...initialData.allPolicies } + // 각 정책 타입에 대해 빈 배열로 초기화 + if (!safePolicies.privacy_policy) safePolicies.privacy_policy = [] + if (!safePolicies.terms_of_service) safePolicies.terms_of_service = [] + return safePolicies + }) + + const [currentPolicies, setCurrentPolicies] = useState(initialData.currentPolicies || {}) + const [isPending, startTransition] = useTransition() + const { toast } = useToast(); + const router = useRouter() + + const policyTypes = [ + { + key: 'privacy_policy', + label: '개인정보 처리방침', + icon: , + description: '개인정보 수집, 이용, 보관 및 파기에 관한 정책' + }, + { + key: 'terms_of_service', + label: '이용약관', + icon: , + description: '서비스 이용 시 준수해야 할 규칙과 조건' + } + ] + + const handleCreateNew = (policyType: string) => { + setEditingPolicy(policyType) + setViewingHistory(null) + setPreviewData(null) + } + + const handleEdit = (policyType: string) => { + setEditingPolicy(policyType) + setViewingHistory(null) + setPreviewData(null) + } + + const handleViewHistory = async (policyType: string) => { + setViewingHistory(policyType) + setEditingPolicy(null) + setPreviewData(null) + + startTransition(async () => { + try { + const history = await getPolicyHistory(policyType) + setPolicies(prev => ({ + ...prev, + [policyType]: history || [] // ✅ null/undefined 방지 + })) + } catch (error) { + console.error('Policy history error:', error) + toast({ + variant: "destructive", + title: "오류", + description: "정책 히스토리를 불러오는데 실패했습니다.", + }) + } + }) + } + + const handlePreview = (policyType: string, content: string, version: string) => { + setPreviewData({ + policyType, + content, + version, + effectiveDate: new Date().toISOString() + }) + setEditingPolicy(null) + setViewingHistory(null) + } + + const handleSavePolicy = async (policyType: string, version: string, content: string) => { + if (!content.trim()) { + toast({ + variant: "destructive", + title: "오류", + description: "정책 내용을 입력해주세요.", + }) + return + } + + startTransition(async () => { + try { + console.log('Saving policy:', { policyType, version }) // ✅ 디버깅 로그 + + const result = await createPolicyVersion({ + policyType: policyType as 'privacy_policy' | 'terms_of_service', + version, + content, + effectiveDate: new Date() + }) + + console.log('Save result:', result) // ✅ 디버깅 로그 + + if (result.success) { + toast({ + title: "성공", + description: "새 정책 버전이 생성되었습니다.", + }) + + // ✅ 상태 업데이트 - 안전하게 처리 + const newPolicy = result.policy + + setPolicies(prev => { + console.log('Updating policies state:', { prev, policyType, newPolicy }) // 디버깅 로그 + return { + ...prev, + [policyType]: [newPolicy, ...(prev[policyType] || [])] // ✅ 안전한 스프레드 + } + }) + + setCurrentPolicies(prev => { + console.log('Updating current policies state:', { prev, policyType, newPolicy }) // 디버깅 로그 + return { + ...prev, + [policyType]: newPolicy + } + }) + + setEditingPolicy(null) + + // ✅ Router refresh를 상태 업데이트 후에 호출 + router.refresh() + } else { + throw new Error(result.error) + } + } catch (error) { + console.error('Save policy error:', error) // ✅ 에러 로그 + toast({ + variant: "destructive", + title: "오류", + description: error?.message || "정책 저장에 실패했습니다.", + }) + } + }) + } + + const handleActivatePolicy = async (policyId: number, policyType: string) => { + startTransition(async () => { + try { + const result = await activatePolicyVersion(policyId) + + if (result.success) { + toast({ + title: "성공", + description: "정책이 활성화되었습니다.", + }) + + // ✅ 현재 정책 업데이트 - 안전하게 처리 + const activatedPolicy = (policies[policyType] || []).find(p => p.id === policyId) + if (activatedPolicy) { + setCurrentPolicies(prev => ({ + ...prev, + [policyType]: activatedPolicy + })) + + // ✅ 정책 목록의 isCurrent 상태 업데이트 + setPolicies(prev => ({ + ...prev, + [policyType]: (prev[policyType] || []).map(p => ({ + ...p, + isCurrent: p.id === policyId + })) + })) + } + + router.refresh() + } else { + throw new Error(result.error) + } + } catch (error) { + console.error('Activate policy error:', error) + toast({ + variant: "destructive", + title: "오류", + description: error?.message || "정책 활성화에 실패했습니다.", + }) + } + }) + } + + const renderMainContent = () => { + const currentPolicy = currentPolicies[currentTab] + const policyInfo = policyTypes.find(p => p.key === currentTab) + + // ✅ 디버깅 정보 + console.log('Render main content:', { + currentTab, + currentPolicy, + editingPolicy, + policiesForTab: policies[currentTab]?.length || 0 + }) + + if (editingPolicy === currentTab) { + return ( + setEditingPolicy(null)} + onPreview={handlePreview} + isLoading={isPending} + /> + ) + } + + if (viewingHistory === currentTab) { + return ( + setEditingPolicy(currentTab)} + onClose={() => setViewingHistory(null)} + isLoading={isPending} + /> + ) + } + + if (previewData && previewData.policyType === currentTab) { + return ( + handleSavePolicy(previewData.policyType, previewData.version, previewData.content)} + onEdit={() => setEditingPolicy(currentTab)} + onClose={() => setPreviewData(null)} + isLoading={isPending} + /> + ) + } + + // 기본 정책 관리 화면 + return ( + + +
+
+ + {policyInfo?.icon} + {policyInfo?.label} + + + {policyInfo?.description} + +
+
+ + {currentPolicy && ( + <> + + + + )} +
+
+
+ + {currentPolicy ? ( +
+ {/* 현재 정책 정보 */} +
+
+ +
+

+ 현재 활성 버전: v{currentPolicy.version} +

+

+ 시행일: {new Date(currentPolicy.effectiveDate).toLocaleDateString('ko-KR')} +

+
+
+ + 활성 + +
+ + {/* 정책 내용 미리보기 */} +
+

정책 내용 미리보기

+
+
1000 ? '...' : '') + }} + /> +
+
+ + {/* 메타 정보 */} +
+
+ + 생성일: {new Date(currentPolicy.createdAt).toLocaleString('ko-KR')} +
+
+ + 버전: {currentPolicy.version} +
+
+
+ ) : ( +
+ +

정책이 등록되지 않았습니다

+

+ {policyInfo?.label}의 첫 번째 버전을 생성해주세요. +

+ +
+ )} + + + ) + } + + return ( +
+
+

정책 편집

+ {isPending && ( +
+ + 처리 중... +
+ )} +
+ + + + {policyTypes.map(policy => ( + + {policy.icon} + {policy.label} + + ))} + + + {policyTypes.map(policy => ( + + {renderMainContent()} + + ))} + +
+ ) +} \ No newline at end of file diff --git a/components/polices/policy-preview.tsx b/components/polices/policy-preview.tsx new file mode 100644 index 00000000..059b2d72 --- /dev/null +++ b/components/polices/policy-preview.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Save, Eye, X, Info, AlertTriangle, Calendar, Clock, FileText, Shield } from 'lucide-react' +import { Badge } from '../ui/badge' + +// ✅ 타입 정의 +interface PolicyPreviewData { + policyType: 'privacy_policy' | 'terms_of_service' + content: string + version: string + effectiveDate: string + id?: number + isCurrent?: boolean + createdAt?: string +} + +interface PolicyPreviewProps { + data: PolicyPreviewData + onSave?: () => void + onEdit?: () => void + onClose: () => void + isLoading?: boolean + mode?: 'preview' | 'view' // ✅ 미리보기 모드 vs 상세보기 모드 +} + +export function PolicyPreview({ + data, + onSave, + onEdit, + onClose, + isLoading = false, + mode = 'preview' +}: PolicyPreviewProps) { + const policyLabels: Record = { + privacy_policy: '개인정보 처리방침', + terms_of_service: '이용약관' + } + + const policyIcons = { + privacy_policy: , + terms_of_service: + } + + // ✅ 모드에 따른 제목과 설명 + const getTitle = () => { + return mode === 'preview' ? '정책 미리보기' : '정책 상세보기' + } + + const getDescription = () => { + if (mode === 'preview') { + return `${policyLabels[data.policyType]} v${data.version} - 저장하면 즉시 활성화됩니다` + } else { + return `${policyLabels[data.policyType]} v${data.version} 상세 내용` + } + } + + // ✅ 상태 텍스트 결정 + const getStatusText = () => { + if (mode === 'preview') return '저장 대기' + if (data.isCurrent) return '현재 활성' + return '비활성' + } + + const getStatusBadge = () => { + if (mode === 'preview') { + return 저장 대기 + } + if (data.isCurrent) { + return 현재 활성 + } + return 비활성 + } + + return ( + + +
+
+ {policyIcons[data.policyType]} +
+ {getTitle()} + + {getDescription()} + +
+
+ +
+
+ + {/* ✅ 정책 메타 정보 */} +
+
+

정책 유형

+

{policyLabels[data.policyType]}

+
+
+

버전

+

v{data.version}

+
+
+

시행일

+

+ {new Date(data.effectiveDate).toLocaleDateString('ko-KR')} +

+
+
+

상태

+
+ {getStatusBadge()} +
+
+ + {/* ✅ 상세보기 모드일 때 추가 정보 */} + {mode === 'view' && data.createdAt && ( + <> +
+

생성일

+

+ {new Date(data.createdAt).toLocaleString('ko-KR')} +

+
+
+

문서 ID

+

#{data.id}

+
+ + )} +
+ + + + {/* ✅ 정책 내용 미리보기 */} +
+
+

정책 내용

+
+ {data.content.replace(/<[^>]*>/g, '').length}자 +
+
+
+
+
+
+ + {/* ✅ 액션 버튼들 */} +
+ {mode === 'preview' ? ( + // 미리보기 모드 버튼들 + <> + + + + + ) : ( + // 상세보기 모드 버튼들 +
+ +
+ )} +
+ + + ) +} \ No newline at end of file -- cgit v1.2.3