From de2ac5a2860bc25180971e7a11f852d9d44675b7 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<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
-- 
cgit v1.2.3

