diff options
Diffstat (limited to 'components/polices/policy-management-client.tsx')
| -rw-r--r-- | components/polices/policy-management-client.tsx | 429 |
1 files changed, 429 insertions, 0 deletions
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<string, any> + allPolicies: Record<string, any[]> + stats: any + } +} + +export function PolicyManagementClient({ initialData }: PolicyManagementClientProps) { + const [currentTab, setCurrentTab] = useState('privacy_policy') + const [editingPolicy, setEditingPolicy] = useState<string | null>(null) + const [viewingHistory, setViewingHistory] = useState<string | null>(null) + const [previewData, setPreviewData] = useState<any>(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: <Shield className="h-4 w-4" />, + description: '개인정보 수집, 이용, 보관 및 파기에 관한 정책' + }, + { + key: 'terms_of_service', + label: '이용약관', + icon: <FileText className="h-4 w-4" />, + 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 ( + <PolicyEditor + policyType={currentTab} + currentPolicy={currentPolicy} + onSave={handleSavePolicy} + onCancel={() => setEditingPolicy(null)} + onPreview={handlePreview} + isLoading={isPending} + /> + ) + } + + if (viewingHistory === currentTab) { + return ( + <PolicyHistory + policyType={currentTab} + policies={policies[currentTab] || []} // ✅ 안전한 기본값 + currentPolicy={currentPolicy} + onActivate={handleActivatePolicy} + onEdit={() => setEditingPolicy(currentTab)} + onClose={() => setViewingHistory(null)} + isLoading={isPending} + /> + ) + } + + if (previewData && previewData.policyType === currentTab) { + return ( + <PolicyPreview + data={previewData} + onSave={() => handleSavePolicy(previewData.policyType, previewData.version, previewData.content)} + onEdit={() => setEditingPolicy(currentTab)} + onClose={() => setPreviewData(null)} + isLoading={isPending} + /> + ) + } + + // 기본 정책 관리 화면 + return ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + {policyInfo?.icon} + {policyInfo?.label} + </CardTitle> + <CardDescription> + {policyInfo?.description} + </CardDescription> + </div> + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleCreateNew(currentTab)} + disabled={isPending} + > + <Plus className="h-4 w-4 mr-1" /> + 새 버전 + </Button> + {currentPolicy && ( + <> + <Button + variant="outline" + size="sm" + onClick={() => handleEdit(currentTab)} + disabled={isPending} + > + <Edit className="h-4 w-4 mr-1" /> + 편집 + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleViewHistory(currentTab)} + disabled={isPending} + > + <History className="h-4 w-4 mr-1" /> + 히스토리 + </Button> + </> + )} + </div> + </div> + </CardHeader> + <CardContent> + {currentPolicy ? ( + <div className="space-y-4"> + {/* 현재 정책 정보 */} + <div className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-lg"> + <div className="flex items-center gap-3"> + <CheckCircle2 className="h-5 w-5 text-green-600" /> + <div> + <p className="font-medium text-green-900"> + 현재 활성 버전: v{currentPolicy.version} + </p> + <p className="text-sm text-green-700"> + 시행일: {new Date(currentPolicy.effectiveDate).toLocaleDateString('ko-KR')} + </p> + </div> + </div> + <Badge variant="secondary" className="bg-green-100 text-green-800"> + 활성 + </Badge> + </div> + + {/* 정책 내용 미리보기 */} + <div className="space-y-2"> + <h4 className="font-medium">정책 내용 미리보기</h4> + <div className="bg-muted/50 p-4 rounded-md max-h-64 overflow-y-auto"> + <div + className="prose prose-sm max-w-none" + dangerouslySetInnerHTML={{ + __html: currentPolicy.content.substring(0, 1000) + (currentPolicy.content.length > 1000 ? '...' : '') + }} + /> + </div> + </div> + + {/* 메타 정보 */} + <div className="grid grid-cols-2 gap-4 pt-4 border-t"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Calendar className="h-4 w-4" /> + 생성일: {new Date(currentPolicy.createdAt).toLocaleString('ko-KR')} + </div> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Clock className="h-4 w-4" /> + 버전: {currentPolicy.version} + </div> + </div> + </div> + ) : ( + <div className="text-center py-12"> + <AlertCircle className="h-12 w-12 text-muted-foreground mx-auto mb-4" /> + <h3 className="text-lg font-medium mb-2">정책이 등록되지 않았습니다</h3> + <p className="text-muted-foreground mb-4"> + {policyInfo?.label}의 첫 번째 버전을 생성해주세요. + </p> + <Button onClick={() => handleCreateNew(currentTab)}> + <Plus className="h-4 w-4 mr-2" /> + 첫 번째 버전 생성 + </Button> + </div> + )} + </CardContent> + </Card> + ) + } + + return ( + <div className="space-y-6"> + <div className="flex items-center justify-between"> + <h2 className="text-2xl font-semibold">정책 편집</h2> + {isPending && ( + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Loader2 className="h-4 w-4 animate-spin" /> + 처리 중... + </div> + )} + </div> + + <Tabs value={currentTab} onValueChange={setCurrentTab}> + <TabsList> + {policyTypes.map(policy => ( + <TabsTrigger + key={policy.key} + value={policy.key} + className="flex items-center gap-2" + > + {policy.icon} + {policy.label} + </TabsTrigger> + ))} + </TabsList> + + {policyTypes.map(policy => ( + <TabsContent key={policy.key} value={policy.key}> + {renderMainContent()} + </TabsContent> + ))} + </Tabs> + </div> + ) +}
\ No newline at end of file |
