zioinfo-web/frontend/src/hooks/useMemberAuth.jsx
DESKTOP-TKLFCPRython c86bd72830 feat(homepage): 회원가입·로그인·SNS 로그인 + 관리자 회원관리
## 홈페이지 (프론트엔드)
- MemberLogin.jsx: 회원가입/로그인 통합 페이지 + 카카오·네이버·구글 SNS 버튼
- MemberAuth.css: 인증 페이지 공통 스타일
- hooks/useMemberAuth.jsx: 회원 인증 상태 훅 + MemberOnly 컴포넌트 (회원 전용 잠금)
- Header.jsx: 로그인/회원가입 버튼 + 로그인 시 이름/로그아웃 표시
- Contact.jsx: 문의 상담 신청 → 회원 전용 (MemberOnly 적용)
- App.jsx: /login, /register 라우트 추가

## 관리자 (Admin)
- AdminMember.jsx: 회원 목록/검색/상태변경/삭제 페이지
- AdminLayout.jsx: '회원 관리' 메뉴 추가
- App.jsx: /admin/members 라우트 추가

## 백엔드 (Spring Boot)
- Member.java: 회원 엔티티 (id/name/email/password/phone/company/role/active)
- MemberRepository.java: 이메일 조회·중복확인·키워드 검색
- MemberController.java: 회원가입·이메일 중복확인·로그인·SNS 로그인·내 정보 CRUD
- AdminController.java: 회원관리 API (목록/상세/상태변경/삭제) + 대시보드에 회원 수 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:49:21 +09:00

84 lines
2.3 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
/**
* 회원 인증 상태 관리 훅.
* - isLoggedIn: 로그인 여부
* - member: 회원 정보
* - requireLogin(redirectPath): 비회원이면 로그인 페이지로 이동
*/
export function useMemberAuth() {
const navigate = useNavigate();
const [member, setMember] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const token = localStorage.getItem('member_token');
const user = localStorage.getItem('member_user');
if (token && user) {
try { setMember(JSON.parse(user)); } catch { /* 손상된 데이터 무시 */ }
}
setLoaded(true);
}, []);
const logout = () => {
localStorage.removeItem('member_token');
localStorage.removeItem('member_user');
setMember(null);
navigate('/');
};
const requireLogin = (redirectPath = window.location.pathname) => {
if (!member) {
navigate(`/login?redirect=${encodeURIComponent(redirectPath)}`);
return false;
}
return true;
};
return {
member,
isLoggedIn: !!member,
loaded,
logout,
requireLogin,
};
}
/**
* 회원 전용 접근 보호 컴포넌트.
* 비로그인 시 잠금 오버레이 표시.
*
* 사용:
* <MemberOnly feature="상담 신청">
* <ConsultForm />
* </MemberOnly>
*/
export function MemberOnly({ children, feature = '이 기능' }) {
const { isLoggedIn, loaded } = useMemberAuth();
const navigate = useNavigate();
if (!loaded) return null;
if (!isLoggedIn) {
return (
<div className="member-guard" style={{ position:'relative', minHeight:120 }}>
{children}
<div className="member-guard-overlay">
<div className="member-guard-icon">🔒</div>
<div className="member-guard-text">{feature} 회원 전용입니다</div>
<div className="member-guard-sub">로그인 이용하실 있습니다</div>
<button
onClick={() => navigate('/login')}
style={{ marginTop:8, padding:'8px 24px', background:'#1a5fd8', color:'#fff',
border:'none', borderRadius:8, cursor:'pointer', fontWeight:600, fontSize:14 }}>
로그인 / 회원가입
</button>
</div>
</div>
);
}
return children;
}