summaryrefslogtreecommitdiff
path: root/components/polices/policy-management-client.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-06 04:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-06 04:23:40 +0000
commitde2ac5a2860bc25180971e7a11f852d9d44675b7 (patch)
treeb931c363f2cb19e177a0a7b17190d5de2a82d709 /components/polices/policy-management-client.tsx
parent6c549b0f264e9be4d60af38f9efc05b189d6849f (diff)
(대표님) 정기평가, 법적검토, 정책, 가입관련 처리 및 관련 컴포넌트 추가, 메뉴 변경
Diffstat (limited to 'components/polices/policy-management-client.tsx')
-rw-r--r--components/polices/policy-management-client.tsx429
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