diff options
Diffstat (limited to 'components/polices')
| -rw-r--r-- | components/polices/policy-editor.tsx | 262 | ||||
| -rw-r--r-- | components/polices/policy-history.tsx | 250 | ||||
| -rw-r--r-- | components/polices/policy-management-client.tsx | 429 | ||||
| -rw-r--r-- | components/polices/policy-preview.tsx | 191 |
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 |
