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/signup/conset-step.tsx | 415 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 components/signup/conset-step.tsx (limited to 'components/signup/conset-step.tsx') diff --git a/components/signup/conset-step.tsx b/components/signup/conset-step.tsx new file mode 100644 index 00000000..3260a7b7 --- /dev/null +++ b/components/signup/conset-step.tsx @@ -0,0 +1,415 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import { ChevronDown, ChevronUp, FileText, Shield, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { getCurrentPolicies } from '@/lib/polices/service'; + +// ✅ 정책 데이터 타입 개선 +interface PolicyData { + id: number; + policyType: 'privacy_policy' | 'terms_of_service'; + version: string; + content: string; // HTML 형태의 리치텍스트 + effectiveDate: string; + isCurrent: boolean; + createdAt: string; +} + +interface PolicyVersions { + privacy_policy?: PolicyData; + terms_of_service?: PolicyData; +} + +interface ConsentStepProps { + data: { + privacy: boolean; + terms: boolean; + marketing: boolean; + }; + onChange: (updater: (prev: any) => any) => void; + onNext: () => void; +} + +export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps) { + const [policyData, setPolicyData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showPrivacyModal, setShowPrivacyModal] = useState(false); + const [showTermsModal, setShowTermsModal] = useState(false); + const [expandedSections, setExpandedSections] = useState>({}); + + const isValid = data.privacy && data.terms; + + // 정책 데이터 로드 + useEffect(() => { + fetchPolicyData(); + }, []); + + const fetchPolicyData = async () => { + try { + setLoading(true); + setError(null); + + const result = await getCurrentPolicies(); + + if (result.success) { + console.log('Policy data loaded:', result.data); + setPolicyData(result.data); + } else { + setError(result.error || '정책 데이터를 불러올 수 없습니다.'); + console.error('Failed to fetch policy data:', result.error); + } + } catch (error) { + const errorMessage = '정책 데이터를 불러오는 중 오류가 발생했습니다.'; + setError(errorMessage); + console.error('Failed to fetch policy data:', error); + } finally { + setLoading(false); + } + }; + + const handleConsentChange = (type: string, checked: boolean) => { + onChange(prev => ({ ...prev, [type]: checked })); + }; + + const toggleSection = (section: string) => { + setExpandedSections(prev => ({ + ...prev, + [section]: !prev[section] + })); + }; + + // ✅ HTML에서 텍스트만 추출하는 함수 + const stripHtmlTags = (html: string): string => { + if (!html) return ''; + + // HTML 태그 제거 + return html + .replace(/<[^>]*>/g, '') // 모든 HTML 태그 제거 + .replace(/ /g, ' ') // non-breaking space 처리 + .replace(/&/g, '&') // HTML entities 처리 + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') // 연속된 공백을 하나로 + .trim(); + }; + + // ✅ 정책 미리보기 텍스트 생성 + const renderPolicyPreview = (policy: PolicyData, maxLength = 200): string => { + if (!policy?.content) return '내용 없음'; + + const textContent = stripHtmlTags(policy.content); + return textContent.length > maxLength + ? textContent.substring(0, maxLength) + '...' + : textContent; + }; + + // ✅ 로딩 상태 + if (loading) { + return ( +
+
+ + 정책 내용을 불러오는 중... +
+
+ ); + } + + // ✅ 에러 상태 + if (error || !policyData) { + return ( +
+
+ {error || '정책 내용을 불러올 수 없습니다.'} +
+ +
+ ); + } + + // ✅ 필수 정책이 없는 경우 + if (!policyData.privacy_policy || !policyData.terms_of_service) { + return ( +
+
+ 일부 정책이 설정되지 않았습니다. 관리자에게 문의해주세요. +
+
+ {!policyData.privacy_policy && '개인정보 처리방침이 없습니다.'}
+ {!policyData.terms_of_service && '이용약관이 없습니다.'} +
+
+ ); + } + + return ( +
+
+

서비스 이용 약관 동의

+

+ 서비스 이용을 위해 다음 약관에 동의해주세요. 각 항목을 클릭하여 상세 내용을 확인할 수 있습니다. +

+
+ +
+ {/* ✅ 개인정보 처리방침 */} + {policyData.privacy_policy && ( + } + title="개인정보 처리방침" + description="개인정보 수집, 이용, 보관 및 파기에 관한 정책입니다." + expanded={expandedSections.privacy} + onToggleExpand={() => toggleSection('privacy')} + onShowModal={() => setShowPrivacyModal(true)} + /> + )} + + + + {/* ✅ 이용약관 */} + {policyData.terms_of_service && ( + } + title="이용약관" + description="서비스 이용 시 준수해야 할 규칙과 조건입니다." + expanded={expandedSections.terms} + onToggleExpand={() => toggleSection('terms')} + onShowModal={() => setShowTermsModal(true)} + /> + )} + + + {/* ✅ 전체 동의 */} +
+
+ { + const checked = e.target.checked; + onChange(() => ({ + privacy: checked, + terms: checked, + marketing: checked + })); + }} + className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + +
+
+
+ +
+ +
+ + {/* ✅ 개인정보 처리방침 상세 모달 */} + {showPrivacyModal && policyData.privacy_policy && ( + setShowPrivacyModal(false)} + onAgree={() => { + onChange(prev => ({ ...prev, privacy: true })); + setShowPrivacyModal(false); + }} + /> + )} + + {/* ✅ 이용약관 상세 모달 */} + {showTermsModal && policyData.terms_of_service && ( + setShowTermsModal(false)} + onAgree={() => { + onChange(prev => ({ ...prev, terms: true })); + setShowTermsModal(false); + }} + /> + )} +
+ ); +} + +// ✅ 개별 정책 동의 섹션 컴포넌트 +interface PolicyConsentSectionProps { + id: string; + type: string; + checked: boolean; + onChange: (type: string, checked: boolean) => void; + policy: PolicyData; + isRequired: boolean; + icon: React.ReactNode; + title: string; + description: string; + expanded: boolean; + onToggleExpand: () => void; + onShowModal: () => void; +} + +function PolicyConsentSection({ + id, type, checked, onChange, policy, isRequired, icon, title, description, + expanded, onToggleExpand, onShowModal +}: PolicyConsentSectionProps) { + // ✅ HTML에서 텍스트 추출 + const renderPolicyPreview = (content: string, maxLength = 300): string => { + if (!content) return '내용 없음'; + + const textContent = content + .replace(/<[^>]*>/g, '') // HTML 태그 제거 + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); + + return textContent.length > maxLength + ? textContent.substring(0, maxLength) + '...' + : textContent; + }; + + return ( +
+ {/* 체크박스와 기본 정보 */} +
+ onChange(type, e.target.checked)} + className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> +
+
+ {icon} + +
+ +

{description}

+ + {/* ✅ 정책 미리보기 - HTML 내용 표시 */} +
+ {renderPolicyPreview(policy.content, expanded ? 1000 : 200)} +
+ + {/* 액션 버튼들 */} +
+ + | + + | + + 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} + +
+
+
+
+ ); +} + +// ✅ 정책 상세 모달 컴포넌트 +interface PolicyModalProps { + policy: PolicyData; + onClose: () => void; + onAgree: () => void; +} + +function PolicyModal({ policy, onClose, onAgree }: PolicyModalProps) { + const getPolicyTitle = (policyType: string): string => { + return policyType === 'privacy_policy' ? '개인정보 처리방침' : '이용약관'; + }; + + return ( +
+
+ {/* 헤더 */} +
+
+

+ {getPolicyTitle(policy.policyType)} +

+

+ 버전 {policy.version} | 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} +

+
+ +
+ + {/* ✅ 내용 - HTML 직접 렌더링 */} + +
+ + + {/* 푸터 */} +
+ + +
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3