summaryrefslogtreecommitdiff
path: root/components/polices
diff options
context:
space:
mode:
Diffstat (limited to 'components/polices')
-rw-r--r--components/polices/policy-editor.tsx262
-rw-r--r--components/polices/policy-history.tsx250
-rw-r--r--components/polices/policy-management-client.tsx429
-rw-r--r--components/polices/policy-preview.tsx191
4 files changed, 1132 insertions, 0 deletions
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<string[]>([])
+
+ 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 `<h1>개인정보 처리방침</h1>
+
+<h2>제1조 (목적)</h2>
+<p>본 개인정보 처리방침은 eVCP(이하 "회사")가 개인정보 보호법 등 관련 법령에 따라 정보주체의 개인정보를 보호하고 이와 관련된 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같은 처리방침을 수립·공개합니다.</p>
+
+<h2>제2조 (개인정보의 수집 및 이용목적)</h2>
+<p>회사는 다음의 목적을 위하여 개인정보를 처리합니다:</p>
+<ul>
+ <li>회원 가입 및 관리</li>
+ <li>서비스 제공 및 계약 이행</li>
+ <li>고객 상담 및 불만 처리</li>
+</ul>
+
+<h2>제3조 (개인정보의 수집항목)</h2>
+<p><strong>필수항목:</strong></p>
+<ul>
+ <li>이메일 주소</li>
+ <li>전화번호</li>
+ <li>회사명</li>
+</ul>
+
+<h2>제4조 (개인정보의 보유 및 이용기간)</h2>
+<p>회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.</p>
+
+<h2>제5조 (정보주체의 권리)</h2>
+<p>정보주체는 회사에 대해 언제든지 다음 각 호의 개인정보 보호 관련 권리를 행사할 수 있습니다:</p>
+<ul>
+ <li>개인정보 처리현황 통지요구</li>
+ <li>개인정보 열람요구</li>
+ <li>개인정보 정정·삭제요구</li>
+ <li>개인정보 처리정지요구</li>
+</ul>`
+ } else {
+ return `<h1>이용약관</h1>
+
+<h2>제1조 (목적)</h2>
+<p>본 약관은 eVCP(이하 "회사")가 제공하는 서비스의 이용조건 및 절차, 회사와 회원 간의 권리, 의무 및 책임사항을 규정함을 목적으로 합니다.</p>
+
+<h2>제2조 (정의)</h2>
+<ul>
+ <li><strong>"서비스"</strong>란 회사가 제공하는 모든 서비스를 의미합니다.</li>
+ <li><strong>"회원"</strong>이란 본 약관에 동의하고 회사와 서비스 이용계약을 체결한 자를 의미합니다.</li>
+ <li><strong>"업체"</strong>란 회사의 파트너로 등록된 법인 또는 개인사업자를 의미합니다.</li>
+</ul>
+
+<h2>제3조 (약관의 효력 및 변경)</h2>
+<p>본 약관은 서비스를 이용하고자 하는 모든 회원에 대하여 그 효력을 발생합니다.</p>
+
+<h2>제4조 (회원가입)</h2>
+<p>회원가입은 신청자가 본 약관의 내용에 대하여 동의를 한 다음 회원가입신청을 하고 회사가 이러한 신청에 대하여 승낙함으로써 체결됩니다.</p>
+
+<h2>제5조 (서비스의 제공)</h2>
+<p>회사는 회원에게 다음과 같은 서비스를 제공합니다:</p>
+<ul>
+ <li>업체 등록 및 관리 서비스</li>
+ <li>문서 관리 서비스</li>
+ <li>견적 제출 서비스</li>
+</ul>`
+ }
+ }
+
+ 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 (
+ <Card>
+ <CardHeader>
+ <CardTitle>
+ {currentPolicy ? '정책 편집' : '새 정책 생성'} - {policyLabels[policyType]}
+ </CardTitle>
+ <CardDescription>
+ {currentPolicy
+ ? `현재 버전 v${currentPolicy.version}을 기반으로 새 버전을 생성합니다.`
+ : `${policyLabels[policyType]}의 첫 번째 버전을 생성합니다.`
+ }
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-6">
+ {/* 경고 메시지 */}
+ <Alert>
+ <Info className="h-4 w-4" />
+ <AlertDescription>
+ 새 버전을 저장하면 즉시 활성화되어 모든 사용자에게 적용됩니다.
+ 저장하기 전에 미리보기로 내용을 확인해주세요.
+ </AlertDescription>
+ </Alert>
+
+ {/* 유효성 검사 오류 */}
+ {validationErrors.length > 0 && (
+ <Alert variant="destructive">
+ <AlertTriangle className="h-4 w-4" />
+ <AlertDescription>
+ <ul className="list-disc list-inside space-y-1">
+ {validationErrors.map((error, index) => (
+ <li key={index}>{error}</li>
+ ))}
+ </ul>
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 버전 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="version">버전</Label>
+ <Input
+ id="version"
+ placeholder="예: 1.1"
+ value={version}
+ onChange={(e) => setVersion(e.target.value)}
+ disabled={isLoading}
+ className="w-32"
+ />
+ <p className="text-xs text-muted-foreground">
+ 형식: 주.부 (예: 1.0, 1.1, 2.0)
+ </p>
+ </div>
+
+ <Separator />
+
+ {/* 정책 내용 편집기 */}
+ <div className="space-y-2">
+ <Label>정책 내용</Label>
+ <div className="border rounded-md">
+ <TiptapEditor
+ content={content}
+ setContent={setContent}
+ disabled={isLoading}
+ height="500px"
+ />
+ </div>
+ <p className="text-xs text-muted-foreground">
+ 리치 텍스트 편집기를 사용하여 정책 내용을 작성하세요.
+ 이미지, 표, 목록 등을 추가할 수 있습니다.
+ </p>
+ </div>
+
+ {/* 액션 버튼들 */}
+ <div className="flex justify-between pt-4">
+ <Button
+ variant="outline"
+ onClick={onCancel}
+ disabled={isLoading}
+ >
+ <X className="h-4 w-4 mr-2" />
+ 취소
+ </Button>
+
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ onClick={handlePreview}
+ disabled={isLoading || !content.trim() || !version.trim()}
+ >
+ <Eye className="h-4 w-4 mr-2" />
+ 미리보기
+ </Button>
+ <Button
+ onClick={handleSave}
+ disabled={isLoading || !content.trim() || !version.trim()}
+ >
+ <Save className="h-4 w-4 mr-2" />
+ {isLoading ? '저장 중...' : '저장'}
+ </Button>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ )
+} \ 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<PolicyData | null>(null) // ✅ 상세보기 상태
+
+ const policyLabels: Record<string, string> = {
+ 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 (
+ <PolicyPreview
+ data={{
+ policyType: viewingPolicy.policyType,
+ content: viewingPolicy.content,
+ version: viewingPolicy.version,
+ effectiveDate: viewingPolicy.effectiveDate,
+ id: viewingPolicy.id,
+ isCurrent: viewingPolicy.isCurrent,
+ createdAt: viewingPolicy.createdAt
+ }}
+ onClose={handleCloseDetail}
+ mode="view"
+ />
+ )
+ }
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle className="flex items-center gap-2">
+ <Clock className="h-5 w-5" />
+ 정책 히스토리
+ </CardTitle>
+ <CardDescription>
+ {policyLabels[policyType]}의 모든 버전을 확인하고 관리합니다
+ </CardDescription>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={onEdit}
+ disabled={isLoading}
+ >
+ 새 버전 생성
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={onClose}
+ disabled={isLoading}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent>
+ {/* ✅ 로딩 상태 */}
+ {isLoading && (
+ <div className="flex items-center justify-center py-8">
+ <div className="text-muted-foreground">히스토리를 불러오는 중...</div>
+ </div>
+ )}
+
+ {/* ✅ 에러 상태 체크 */}
+ {!isLoading && safePolicies && !Array.isArray(safePolicies) && (
+ <Alert variant="destructive" className="mb-4">
+ <AlertTriangle className="h-4 w-4" />
+ <AlertDescription>
+ 정책 데이터 형식이 올바르지 않습니다. (받은 데이터: {typeof safePolicies})
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* ✅ 정책 목록 */}
+ {!isLoading && (
+ <div className="space-y-4">
+ {safePolicies.length === 0 ? (
+ <div className="text-center py-8">
+ <div className="text-muted-foreground mb-2">
+ <Info className="h-8 w-8 mx-auto mb-2 opacity-50" />
+ <p>등록된 정책 버전이 없습니다.</p>
+ </div>
+ <Button onClick={onEdit} size="sm">
+ 첫 번째 버전 생성하기
+ </Button>
+ </div>
+ ) : (
+ <>
+ {/* ✅ 정책 개수 표시 */}
+ <div className="flex items-center justify-between mb-4">
+ <div className="text-sm text-muted-foreground">
+ 총 {safePolicies.length}개 버전
+ </div>
+ {currentPolicy && (
+ <Badge variant="outline" className="text-xs">
+ 현재: v{currentPolicy.version}
+ </Badge>
+ )}
+ </div>
+
+ {/* ✅ 정책 목록 렌더링 */}
+ {safePolicies.map((policy: PolicyData) => (
+ <div
+ key={policy.id}
+ className={`p-4 border rounded-lg transition-colors ${
+ policy.isCurrent
+ ? 'border-green-200 bg-green-50'
+ : 'border-border hover:bg-muted/30'
+ }`}
+ >
+ <div className="flex items-start justify-between">
+ <div className="flex-1 min-w-0">
+ {/* ✅ 헤더 */}
+ <div className="flex items-center gap-2 mb-2">
+ <h4 className="font-medium text-base">
+ 버전 {policy.version}
+ </h4>
+ {policy.isCurrent && (
+ <Badge className="bg-green-100 text-green-800 text-xs">
+ 현재 활성
+ </Badge>
+ )}
+ </div>
+
+ {/* ✅ 메타 정보 */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3 text-sm text-muted-foreground">
+ <div className="flex items-center gap-1">
+ <Calendar className="h-3 w-3" />
+ 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')}
+ </div>
+ <div className="flex items-center gap-1">
+ <Clock className="h-3 w-3" />
+ 생성일: {new Date(policy.createdAt).toLocaleDateString('ko-KR')}
+ </div>
+ </div>
+
+ {/* ✅ 내용 미리보기 */}
+ <div className="mt-2">
+ <div className="text-sm bg-muted/50 p-3 rounded border max-h-20 overflow-hidden">
+ {getContentPreview(policy.content)}
+ </div>
+ </div>
+ </div>
+
+ {/* ✅ 액션 버튼들 */}
+ <div className="flex flex-col gap-2 ml-4 flex-shrink-0">
+ {!policy.isCurrent && (
+ <Button
+ size="sm"
+ onClick={() => onActivate(policy.id, policyType)}
+ disabled={isLoading}
+ className="whitespace-nowrap"
+ >
+ 활성화
+ </Button>
+ )}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleViewDetail(policy)}
+ disabled={isLoading}
+ className="whitespace-nowrap"
+ >
+ <Eye className="h-3 w-3 mr-1" />
+ 상세보기
+ </Button>
+ </div>
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )
+} \ 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<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
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<string, string> = {
+ privacy_policy: '개인정보 처리방침',
+ terms_of_service: '이용약관'
+ }
+
+ const policyIcons = {
+ privacy_policy: <Shield className="h-5 w-5" />,
+ terms_of_service: <FileText className="h-5 w-5" />
+ }
+
+ // ✅ 모드에 따른 제목과 설명
+ 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 <Badge variant="outline" className="text-orange-600">저장 대기</Badge>
+ }
+ if (data.isCurrent) {
+ return <Badge className="bg-green-100 text-green-800">현재 활성</Badge>
+ }
+ return <Badge variant="secondary">비활성</Badge>
+ }
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ {policyIcons[data.policyType]}
+ <div>
+ <CardTitle>{getTitle()}</CardTitle>
+ <CardDescription>
+ {getDescription()}
+ </CardDescription>
+ </div>
+ </div>
+ <Button variant="outline" size="sm" onClick={onClose}>
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ </CardHeader>
+ <CardContent className="space-y-6">
+ {/* ✅ 정책 메타 정보 */}
+ <div className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg">
+ <div>
+ <p className="text-sm font-medium">정책 유형</p>
+ <p className="text-sm text-muted-foreground">{policyLabels[data.policyType]}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium">버전</p>
+ <p className="text-sm text-muted-foreground">v{data.version}</p>
+ </div>
+ <div>
+ <p className="text-sm font-medium">시행일</p>
+ <p className="text-sm text-muted-foreground">
+ {new Date(data.effectiveDate).toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ <div>
+ <p className="text-sm font-medium">상태</p>
+ <div className="flex items-center gap-2">
+ {getStatusBadge()}
+ </div>
+ </div>
+
+ {/* ✅ 상세보기 모드일 때 추가 정보 */}
+ {mode === 'view' && data.createdAt && (
+ <>
+ <div>
+ <p className="text-sm font-medium">생성일</p>
+ <p className="text-sm text-muted-foreground">
+ {new Date(data.createdAt).toLocaleString('ko-KR')}
+ </p>
+ </div>
+ <div>
+ <p className="text-sm font-medium">문서 ID</p>
+ <p className="text-sm text-muted-foreground">#{data.id}</p>
+ </div>
+ </>
+ )}
+ </div>
+
+ <Separator />
+
+ {/* ✅ 정책 내용 미리보기 */}
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <h4 className="font-medium">정책 내용</h4>
+ <div className="text-xs text-muted-foreground">
+ {data.content.replace(/<[^>]*>/g, '').length}자
+ </div>
+ </div>
+ <div className="bg-white border rounded-md p-6 max-h-96 overflow-y-auto">
+ <div
+ className="prose prose-sm max-w-none"
+ dangerouslySetInnerHTML={{ __html: data.content }}
+ />
+ </div>
+ </div>
+
+ {/* ✅ 액션 버튼들 */}
+ <div className="flex justify-between pt-4">
+ {mode === 'preview' ? (
+ // 미리보기 모드 버튼들
+ <>
+ <Button
+ variant="outline"
+ onClick={onEdit}
+ disabled={isLoading}
+ >
+ 편집으로 돌아가기
+ </Button>
+
+ <Button
+ onClick={onSave}
+ disabled={isLoading}
+ className="bg-green-600 hover:bg-green-700"
+ >
+ <Save className="h-4 w-4 mr-2" />
+ {isLoading ? '저장 중...' : '저장 및 활성화'}
+ </Button>
+ </>
+ ) : (
+ // 상세보기 모드 버튼들
+ <div className="w-full flex justify-end">
+ <Button
+ variant="outline"
+ onClick={onClose}
+ disabled={isLoading}
+ >
+ 닫기
+ </Button>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )
+} \ No newline at end of file