diff --git a/src/assets/images/deleteicon.svg b/src/assets/images/deleteicon.svg new file mode 100644 index 0000000..a3058df --- /dev/null +++ b/src/assets/images/deleteicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/editicon.svg b/src/assets/images/editicon.svg new file mode 100644 index 0000000..9bbb489 --- /dev/null +++ b/src/assets/images/editicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/hooks/useScrollReveal.js b/src/hooks/useScrollReveal.js index 46d1767..4e0dd76 100644 --- a/src/hooks/useScrollReveal.js +++ b/src/hooks/useScrollReveal.js @@ -6,7 +6,8 @@ import { useRef, useEffect, useCallback } from 'react'; * @returns {Function} ref 콜백 — 감시할 DOM 요소에 ref={revealRef} 로 전달 */ export default function useScrollReveal() { - const revealRefs = useRef([]); + const observerRef = useRef(null); + const elementsRef = useRef([]); useEffect(() => { const observer = new IntersectionObserver( @@ -20,14 +21,21 @@ export default function useScrollReveal() { }, { threshold: 0.1 } ); + observerRef.current = observer; - revealRefs.current.forEach((element) => element && observer.observe(element)); - return () => observer.disconnect(); + elementsRef.current.forEach((element) => observer.observe(element)); + + return () => { + observer.disconnect(); + observerRef.current = null; + }; }, []); return useCallback((element) => { - if (element && !revealRefs.current.includes(element)) { - revealRefs.current.push(element); + if (!element) return; + if (!elementsRef.current.includes(element)) { + elementsRef.current.push(element); } + observerRef.current?.observe(element); }, []); } diff --git a/src/pages/admin/AdminPage.jsx b/src/pages/admin/AdminPage.jsx index 9a9173d..73312b1 100644 --- a/src/pages/admin/AdminPage.jsx +++ b/src/pages/admin/AdminPage.jsx @@ -1,8 +1,21 @@ +import ActivitiesSection from '@/pages/visitor/main/ActivitiesSection'; +import TimelineSection from '@/pages/visitor/main/TimelineSection'; +import RecruitSection from '@/pages/visitor/main/RecruitSection'; +import HeroSection from '../visitor/main/HeroSection'; + +/** + * 관리자 홈 콘텐츠 관리 페이지. + * 주요 활동/ONE 활동 현황/신입 부원 모집 섹션을 그대로 재사용한다. + * isEditable을 켜서 렌더링하므로, 관리자 로그인 상태(isAuthenticated)일 때만 수정/삭제 아이콘을 노출한다. + * 방문자용 HomePage는 isEditable을 넘기지 않아 로그인 여부와 무관하게 항상 일반 화면만 보인다. + */ function AdminPage() { return (
-

관리자 페이지

-

신청 부원 정보, 수상경력, 연간 계획, 동아리 소개, 명부 관리 (CRUD)

+ + + +
); } diff --git a/src/pages/visitor/main/ActivitiesSection.jsx b/src/pages/visitor/main/ActivitiesSection.jsx index 060b6d0..87c2f1a 100644 --- a/src/pages/visitor/main/ActivitiesSection.jsx +++ b/src/pages/visitor/main/ActivitiesSection.jsx @@ -1,8 +1,60 @@ +import { useState } from 'react'; import useScrollReveal from '@/hooks/useScrollReveal'; -import { ACTIVITY_LIST } from '@/constants/homeData'; +import useAuthStore from '@/stores/authStore'; +import useHomeContentStore from '@/stores/homeContentStore'; +import editIcon from '@/assets/images/editicon.svg'; +import deleteIcon from '@/assets/images/deleteicon.svg'; -export default function ActivitiesSection() { +export default function ActivitiesSection({ isEditable = false }) { const revealRef = useScrollReveal(); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated) && isEditable; + const activities = useHomeContentStore((state) => state.activities); + const updateActivity = useHomeContentStore((state) => state.updateActivity); + + const [editingIndex, setEditingIndex] = useState(null); + const [draftTitle, setDraftTitle] = useState(''); + const [draftDescription, setDraftDescription] = useState(''); + + const startEdit = (index) => { + setEditingIndex(index); + setDraftTitle(activities[index].title); + setDraftDescription(activities[index].description); + }; + + const DESCRIPTION_LINE_LENGTH = 30; + + const handleDescriptionChange = (value) => { + let [firstLine, ...restLines] = value.split('\n'); + let secondLine = restLines.join(''); + + if (firstLine.length > DESCRIPTION_LINE_LENGTH) { + secondLine = firstLine.slice(DESCRIPTION_LINE_LENGTH) + secondLine; + firstLine = firstLine.slice(0, DESCRIPTION_LINE_LENGTH); + } + secondLine = secondLine.slice(0, DESCRIPTION_LINE_LENGTH); + + setDraftDescription(restLines.length > 0 || secondLine ? `${firstLine}\n${secondLine}` : firstLine); + }; + + const handleDescriptionKeyDown = (event) => { + if (event.key === 'Enter' && draftDescription.includes('\n')) { + event.preventDefault(); + } + }; + + const handleSaveClick = (index) => { + if (!draftTitle.trim() || !draftDescription.trim()) { + alert('내용을 입력해 주세요.'); + return; + } + updateActivity(index, { title: draftTitle, description: draftDescription }); + setEditingIndex(null); + }; + + const handleDeleteContent = (index) => { + updateActivity(index, { title: '', description: '' }); + setEditingIndex((previous) => (previous === index ? null : previous)); + }; return (
@@ -15,27 +67,80 @@ export default function ActivitiesSection() { {/* 2×2 카드 그리드 */}
- {ACTIVITY_LIST.map((activity, index) => ( -
- {/* 원형 아이콘 컨테이너 */} -
- {activity.title} + {activities.map((activity, index) => { + const isEditing = editingIndex === index; + return ( +
+ {isAuthenticated && ( +
+ + +
+ )} + + {/* 원형 아이콘 컨테이너 */} +
+ {activity.title} +
+ + {isEditing ? ( + <> + setDraftTitle(event.target.value)} + placeholder="활동 제목" + maxLength={20} + className="mb-5 w-full rounded-md border border-line px-3 py-2 text-center text-lg font-bold text-ink" + /> +