summaryrefslogtreecommitdiff
path: root/components/signup/conset-step.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/signup/conset-step.tsx
parent6c549b0f264e9be4d60af38f9efc05b189d6849f (diff)
(대표님) 정기평가, 법적검토, 정책, 가입관련 처리 및 관련 컴포넌트 추가, 메뉴 변경
Diffstat (limited to 'components/signup/conset-step.tsx')
-rw-r--r--components/signup/conset-step.tsx415
1 files changed, 415 insertions, 0 deletions
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<PolicyVersions | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const [showPrivacyModal, setShowPrivacyModal] = useState(false);
+ const [showTermsModal, setShowTermsModal] = useState(false);
+ const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
+
+ 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(/&nbsp;/g, ' ') // non-breaking space 처리
+ .replace(/&amp;/g, '&') // HTML entities 처리
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"')
+ .replace(/&#39;/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 (
+ <div className="flex items-center justify-center p-8">
+ <div className="flex items-center gap-2 text-gray-600">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ 정책 내용을 불러오는 중...
+ </div>
+ </div>
+ );
+ }
+
+ // ✅ 에러 상태
+ if (error || !policyData) {
+ return (
+ <div className="text-center p-8">
+ <div className="text-red-600 mb-4">
+ {error || '정책 내용을 불러올 수 없습니다.'}
+ </div>
+ <Button onClick={fetchPolicyData} variant="outline">
+ 다시 시도
+ </Button>
+ </div>
+ );
+ }
+
+ // ✅ 필수 정책이 없는 경우
+ if (!policyData.privacy_policy || !policyData.terms_of_service) {
+ return (
+ <div className="text-center p-8">
+ <div className="text-amber-600 mb-4">
+ 일부 정책이 설정되지 않았습니다. 관리자에게 문의해주세요.
+ </div>
+ <div className="text-sm text-gray-500">
+ {!policyData.privacy_policy && '개인정보 처리방침이 없습니다.'}<br />
+ {!policyData.terms_of_service && '이용약관이 없습니다.'}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <h2 className="text-xl font-semibold mb-2">서비스 이용 약관 동의</h2>
+ <p className="text-gray-600 text-sm">
+ 서비스 이용을 위해 다음 약관에 동의해주세요. 각 항목을 클릭하여 상세 내용을 확인할 수 있습니다.
+ </p>
+ </div>
+
+ <div className="space-y-4">
+ {/* ✅ 개인정보 처리방침 */}
+ {policyData.privacy_policy && (
+ <PolicyConsentSection
+ id="privacy-consent"
+ type="privacy"
+ checked={data.privacy}
+ onChange={handleConsentChange}
+ policy={policyData.privacy_policy}
+ isRequired={true}
+ icon={<Shield className="w-4 h-4" />}
+ title="개인정보 처리방침"
+ description="개인정보 수집, 이용, 보관 및 파기에 관한 정책입니다."
+ expanded={expandedSections.privacy}
+ onToggleExpand={() => toggleSection('privacy')}
+ onShowModal={() => setShowPrivacyModal(true)}
+ />
+ )}
+
+ <Separator />
+
+ {/* ✅ 이용약관 */}
+ {policyData.terms_of_service && (
+ <PolicyConsentSection
+ id="terms-consent"
+ type="terms"
+ checked={data.terms}
+ onChange={handleConsentChange}
+ policy={policyData.terms_of_service}
+ isRequired={true}
+ icon={<FileText className="w-4 h-4" />}
+ title="이용약관"
+ description="서비스 이용 시 준수해야 할 규칙과 조건입니다."
+ expanded={expandedSections.terms}
+ onToggleExpand={() => toggleSection('terms')}
+ onShowModal={() => setShowTermsModal(true)}
+ />
+ )}
+
+
+ {/* ✅ 전체 동의 */}
+ <div className="pt-4 border-t bg-gray-50 p-4 rounded-lg">
+ <div className="flex items-center space-x-3">
+ <input
+ type="checkbox"
+ id="all-consent"
+ checked={data.privacy && data.terms && data.marketing}
+ onChange={(e) => {
+ 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"
+ />
+ <label htmlFor="all-consent" className="text-base font-medium cursor-pointer">
+ 위 내용에 모두 동의합니다
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex justify-end">
+ <Button onClick={onNext} disabled={!isValid} size="lg">
+ 다음 단계로
+ </Button>
+ </div>
+
+ {/* ✅ 개인정보 처리방침 상세 모달 */}
+ {showPrivacyModal && policyData.privacy_policy && (
+ <PolicyModal
+ policy={policyData.privacy_policy}
+ onClose={() => setShowPrivacyModal(false)}
+ onAgree={() => {
+ onChange(prev => ({ ...prev, privacy: true }));
+ setShowPrivacyModal(false);
+ }}
+ />
+ )}
+
+ {/* ✅ 이용약관 상세 모달 */}
+ {showTermsModal && policyData.terms_of_service && (
+ <PolicyModal
+ policy={policyData.terms_of_service}
+ onClose={() => setShowTermsModal(false)}
+ onAgree={() => {
+ onChange(prev => ({ ...prev, terms: true }));
+ setShowTermsModal(false);
+ }}
+ />
+ )}
+ </div>
+ );
+}
+
+// ✅ 개별 정책 동의 섹션 컴포넌트
+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(/&nbsp;/g, ' ')
+ .replace(/&amp;/g, '&')
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"')
+ .replace(/&#39;/g, "'")
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ return textContent.length > maxLength
+ ? textContent.substring(0, maxLength) + '...'
+ : textContent;
+ };
+
+ return (
+ <div className="border rounded-lg p-4 hover:bg-gray-50 transition-colors">
+ {/* 체크박스와 기본 정보 */}
+ <div className="flex items-start space-x-3">
+ <input
+ type="checkbox"
+ id={id}
+ checked={checked}
+ onChange={(e) => onChange(type, e.target.checked)}
+ className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
+ />
+ <div className="flex-1">
+ <div className="flex items-center space-x-2 mb-1">
+ {icon}
+ <label htmlFor={id} className="text-sm font-medium cursor-pointer">
+ <span className={isRequired ? "text-red-500" : "text-blue-500"}>
+ [{isRequired ? "필수" : "선택"}]
+ </span> {title} (v{policy.version})
+ </label>
+ </div>
+
+ <p className="text-xs text-gray-600 mb-2">{description}</p>
+
+ {/* ✅ 정책 미리보기 - HTML 내용 표시 */}
+ <div className="bg-gray-50 p-3 rounded text-xs text-gray-700 mb-2">
+ {renderPolicyPreview(policy.content, expanded ? 1000 : 200)}
+ </div>
+
+ {/* 액션 버튼들 */}
+ <div className="flex items-center space-x-3 text-xs">
+ <button
+ type="button"
+ onClick={onToggleExpand}
+ className="flex items-center space-x-1 text-blue-600 hover:underline"
+ >
+ {expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
+ <span>{expanded ? '간략히' : '더보기'}</span>
+ </button>
+ <span className="text-gray-400">|</span>
+ <button
+ type="button"
+ onClick={onShowModal}
+ className="text-blue-600 hover:underline"
+ >
+ 전문보기
+ </button>
+ <span className="text-gray-400">|</span>
+ <span className="text-gray-500">
+ 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+// ✅ 정책 상세 모달 컴포넌트
+interface PolicyModalProps {
+ policy: PolicyData;
+ onClose: () => void;
+ onAgree: () => void;
+}
+
+function PolicyModal({ policy, onClose, onAgree }: PolicyModalProps) {
+ const getPolicyTitle = (policyType: string): string => {
+ return policyType === 'privacy_policy' ? '개인정보 처리방침' : '이용약관';
+ };
+
+ return (
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
+ <div className="bg-white rounded-lg w-full max-w-4xl max-h-[90vh] flex flex-col">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between p-6 border-b">
+ <div>
+ <h3 className="text-xl font-semibold">
+ {getPolicyTitle(policy.policyType)}
+ </h3>
+ <p className="text-sm text-gray-600 mt-1">
+ 버전 {policy.version} | 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ <button
+ onClick={onClose}
+ className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
+ >
+ ×
+ </button>
+ </div>
+
+ {/* ✅ 내용 - HTML 직접 렌더링 */}
+ <ScrollArea className="flex-1 p-6">
+ <div
+ className="prose prose-sm max-w-none text-gray-700 leading-relaxed"
+ dangerouslySetInnerHTML={{ __html: policy.content }}
+ />
+ </ScrollArea>
+
+ {/* 푸터 */}
+ <div className="flex gap-3 p-6 border-t bg-gray-50">
+ <Button
+ variant="outline"
+ onClick={onClose}
+ className="flex-1"
+ >
+ 닫기
+ </Button>
+ <Button
+ onClick={onAgree}
+ className="flex-1"
+ >
+ 동의하고 닫기
+ </Button>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file