From 42b22a34329fb3d8853e872bdf0f2da2bfc9fb0a Mon Sep 17 00:00:00 2001 From: wnsgur393 <2021301022@skuniv.ac.kr> Date: Sat, 23 Aug 2025 16:05:09 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B3=B5=EB=8F=99=20?= =?UTF-8?q?=EC=A0=84=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/exhibition/ExhibitionList.jsx | 2 +- .../exhibition/exhibitionList.module.css | 14 +- .../components/museum/InvitationSection.jsx | 33 ++ .../museum/invitationSection.module.css | 42 ++ .../museum/pages/ExhibitionInvitationPage.jsx | 137 +++++ .../pages/ExhibitionParticipantPage.jsx | 237 ++++---- .../museum/pages/ExhibitionUploadPage.jsx | 25 +- .../museum/pages/SharedLibraryEntryPage.jsx | 283 ++++++++++ .../pages/SharedLibrarySelectionPage.jsx | 99 ++++ .../pages/exhibitionInvitationPage.module.css | 166 ++++++ .../exhibitionParticipantPage.module.css | 223 +++++--- .../pages/exhibitionUploadPage.module.css | 107 +++- .../pages/sharedLibraryEntryPage.module.css | 531 ++++++++++++++++++ .../sharedLibrarySelectionPage.module.css | 120 ++++ src/pages/MuseumPage.jsx | 12 +- src/routers/index.jsx | 6 + src/stores/userStore.js | 17 +- 17 files changed, 1825 insertions(+), 229 deletions(-) create mode 100644 src/components/museum/components/museum/InvitationSection.jsx create mode 100644 src/components/museum/components/museum/invitationSection.module.css create mode 100644 src/components/museum/pages/ExhibitionInvitationPage.jsx create mode 100644 src/components/museum/pages/SharedLibraryEntryPage.jsx create mode 100644 src/components/museum/pages/SharedLibrarySelectionPage.jsx create mode 100644 src/components/museum/pages/exhibitionInvitationPage.module.css create mode 100644 src/components/museum/pages/sharedLibraryEntryPage.module.css create mode 100644 src/components/museum/pages/sharedLibrarySelectionPage.module.css diff --git a/src/components/museum/components/exhibition/ExhibitionList.jsx b/src/components/museum/components/exhibition/ExhibitionList.jsx index c194c03..5271389 100644 --- a/src/components/museum/components/exhibition/ExhibitionList.jsx +++ b/src/components/museum/components/exhibition/ExhibitionList.jsx @@ -99,7 +99,7 @@ export default function ExhibitionList({ if (isCompletelyEmpty) { return ( -
+
{/* 헤더 */}
diff --git a/src/components/museum/components/exhibition/exhibitionList.module.css b/src/components/museum/components/exhibition/exhibitionList.module.css index 83bfecd..02c0832 100644 --- a/src/components/museum/components/exhibition/exhibitionList.module.css +++ b/src/components/museum/components/exhibition/exhibitionList.module.css @@ -1,12 +1,22 @@ .container { - min-height: 100vh; background-color: #f6f1eb; - padding: 0 32px 100px; + padding: 0 32px; position: relative; max-width: 430px; margin: 0 auto; } +/* 빈 상태가 아닐 때만 최소 높이 적용 */ +.container:not(.emptyContainer) { + min-height: 100vh; +} + +/* 빈 상태일 때는 최소 높이 제거 */ +.emptyContainer { + min-height: auto; + height: auto; +} + /* 헤더 */ .header { padding: 10px 0 20px; diff --git a/src/components/museum/components/museum/InvitationSection.jsx b/src/components/museum/components/museum/InvitationSection.jsx new file mode 100644 index 0000000..78a503e --- /dev/null +++ b/src/components/museum/components/museum/InvitationSection.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './invitationSection.module.css'; + +export default function InvitationSection({ hasInvitation, hasSharedLibraryRequest, invitationCount = 0 }) { + const navigate = useNavigate(); + + const handleInvitationClick = () => { + navigate('/exhibition/invitations'); + }; + + const handleSharedLibraryClick = () => { + navigate('/exhibition/shared-library-selection'); + }; + + return ( +
+ {/* 공동 전시 참여 요청 알림 */} + {hasInvitation && ( +
+ 공동 전시 참여 요청 ({invitationCount}개) +
+ )} + + {/* 작품 공유 라이브러리 등록 안내 */} + {hasSharedLibraryRequest && ( +
+ 작품을 공유 라이브러리에 등록해주세요 +
+ )} +
+ ); +} diff --git a/src/components/museum/components/museum/invitationSection.module.css b/src/components/museum/components/museum/invitationSection.module.css new file mode 100644 index 0000000..6f3ea15 --- /dev/null +++ b/src/components/museum/components/museum/invitationSection.module.css @@ -0,0 +1,42 @@ +.invitationSection { + margin-bottom: 20px; +} + +.invitationAlert { + height: 48px; + width: 360px; + border: 2px solid #F37021; + background: #CEDDE4; + border-right: none; + display: flex; + align-items: center; + padding: 0 40px; + margin-top: 12px; + margin-bottom: 12px; + margin-left: 70px; + color: #000; + font-family: 'Pretendard', sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 19px; + letter-spacing: -0.4px; + cursor: pointer; +} + +.sharedLibraryAlert { + height: 48px; + width: 360px; + border: 2px solid #F37021; + background: #CEDDE4; + border-right: none; + margin: 12px 0 0 70px; + display: flex; + align-items: center; + padding: 0 40px; + font-family: 'Pretendard', sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 19px; + letter-spacing: -0.4px; + cursor: pointer; +} diff --git a/src/components/museum/pages/ExhibitionInvitationPage.jsx b/src/components/museum/pages/ExhibitionInvitationPage.jsx new file mode 100644 index 0000000..a462333 --- /dev/null +++ b/src/components/museum/pages/ExhibitionInvitationPage.jsx @@ -0,0 +1,137 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import chevronLeft from '@/assets/museum/chevron-left.png'; +import checkImage from '@/assets/museum/check.png'; +import styles from './exhibitionInvitationPage.module.css'; +import useUserStore from '@/stores/userStore'; + +export default function ExhibitionInvitationPage() { + const navigate = useNavigate(); + const { updateInvitation } = useUserStore(); + + // 상태 관리 + const [invitations, setInvitations] = useState([]); + const [selectedInvitations, setSelectedInvitations] = useState([]); + + // 더미 초대 데이터 (실제로는 API에서 가져올 예정) + const dummyInvitations = [ + { + id: 1, + exhibitionTitle: '2025 성북구 작가 단체전', + exhibitionImage: null, + status: 'pending' // pending, accepted, declined + }, + { + id: 2, + exhibitionTitle: '2025 성북구 작가 단체전', + exhibitionImage: null, + status: 'pending' + }, + { + id: 3, + exhibitionTitle: '2025 강남구 작가 단체전', + exhibitionImage: null, + status: 'pending' + } + ]; + + useEffect(() => { + // 초대 목록 가져오기 (실제로는 API 호출) + setInvitations(dummyInvitations); + }, []); + + const handleBack = () => { + navigate('/museum'); + }; + + const handleInvitationSelect = (invitationId) => { + const isSelected = selectedInvitations.includes(invitationId); + + if (isSelected) { + setSelectedInvitations(prev => prev.filter(id => id !== invitationId)); + } else { + setSelectedInvitations(prev => [...prev, invitationId]); + } + }; + + const handleAcceptInvitations = () => { + if (selectedInvitations.length > 0) { + // 선택된 초대들을 수락 처리 + console.log('수락할 초대:', selectedInvitations); + + // TODO: API 호출로 초대 수락 처리 + + // userStore 상태 업데이트 + updateInvitation({ + hasInvitation: false, // 공동 전시 참여 요청 알림 숨김 + hasSharedLibraryRequest: true, // 작품 공유 라이브러리 등록 안내 표시 + invitationCount: 0 // 요청 개수 0으로 설정 + }); + + // 내 전시장 페이지로 돌아가기 + navigate('/museum'); + } else { + alert('수락할 초대를 선택해주세요.'); + } + }; + + const isInvitationSelected = (invitationId) => { + return selectedInvitations.includes(invitationId); + }; + + return ( +
+ {/* 헤더 */} +
+
+ +

참여 요청 받은 전시

+
+
+ + {/* 초대 목록 */} +
+ {invitations.map((invitation) => ( +
+
+
+ {invitation.exhibitionImage ? ( + 전시 이미지 + ) : ( +
+ )} +
+
+ {invitation.exhibitionTitle} +
+
+ + {/* 커스텀 체크박스 - check.png 이미지 사용 */} +
handleInvitationSelect(invitation.id)} + > + {isInvitationSelected(invitation.id) && ( + 체크됨 + )} +
+
+ ))} +
+ + {/* 수락하기 버튼 */} + +
+ ); +} diff --git a/src/components/museum/pages/ExhibitionParticipantPage.jsx b/src/components/museum/pages/ExhibitionParticipantPage.jsx index 1524470..13fc8fa 100644 --- a/src/components/museum/pages/ExhibitionParticipantPage.jsx +++ b/src/components/museum/pages/ExhibitionParticipantPage.jsx @@ -1,6 +1,5 @@ import { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import useUserStore from '@/stores/userStore'; import chevronLeft from '@/assets/museum/chevron-left.png'; import searchIcon from '@/assets/footer/search.svg'; import styles from './exhibitionParticipantPage.module.css'; @@ -8,86 +7,77 @@ import styles from './exhibitionParticipantPage.module.css'; export default function ExhibitionParticipantPage() { const navigate = useNavigate(); const location = useLocation(); - const { user } = useUserStore(); // 상태 관리 const [searchQuery, setSearchQuery] = useState(''); - const [selectedParticipants, setSelectedParticipants] = useState([]); const [searchResults, setSearchResults] = useState([]); - const [showNotification, setShowNotification] = useState(false); + const [selectedParticipants, setSelectedParticipants] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); // URL state에서 전시 정보 받아오기 - useEffect(() => { - if (location.state?.exhibitionData) { - console.log('전시 정보:', location.state.exhibitionData); - } - }, [location.state]); + const exhibitionData = location.state?.exhibitionData || {}; - // 전시 정보가 없으면 전시 등록 페이지로 리다이렉트 - useEffect(() => { - if (!location.state?.exhibitionData) { - navigate('/exhibition/upload'); - } - }, [location.state, navigate]); - - const handleBack = () => { - navigate('/exhibition/upload', { - state: { - exhibitionData: location.state?.exhibitionData - } - }); - }; + // 더미 사용자 데이터 (실제로는 API에서 가져올 예정) + const dummyUsers = [ + { id: 1, username: 'kimdangdeng', displayName: '김땡땡', profileImage: null }, + { id: 2, username: 'simonkim', displayName: '정땡땡', profileImage: null }, + { id: 3, username: 'kimchiman', displayName: 'kimman', profileImage: null }, + ]; - const handleSearchChange = (e) => { - const query = e.target.value; - setSearchQuery(query); - + useEffect(() => { // 검색어가 있을 때만 검색 결과 표시 - if (query.trim()) { - // 실제로는 API 호출을 통해 사용자 검색 - // 여기서는 더미 데이터 사용 - const dummyResults = [ - { id: 1, name: '김땡땡', username: 'kimdangdeng', profileImage: null }, - { id: 2, name: '정땡땡', username: 'simonkim', profileImage: null }, - { id: 3, name: 'kimman', username: 'kimchiman', profileImage: null } - ].filter(user => - user.name.toLowerCase().includes(query.toLowerCase()) || - user.username.toLowerCase().includes(query.toLowerCase()) + if (searchQuery.trim()) { + const filtered = dummyUsers.filter(user => + user.username.toLowerCase().includes(searchQuery.toLowerCase()) || + user.displayName.toLowerCase().includes(searchQuery.toLowerCase()) ); - setSearchResults(dummyResults); + setSearchResults(filtered); + setIsSearching(true); } else { setSearchResults([]); + setIsSearching(false); } + }, [searchQuery]); + + const handleBack = () => { + navigate('/museum'); }; - const handleParticipantToggle = (participant) => { - setSelectedParticipants(prev => { - const isSelected = prev.find(p => p.id === participant.id); - if (isSelected) { - return prev.filter(p => p.id !== participant.id); - } else { - return [...prev, participant]; - } - }); + const handleSearchChange = (e) => { + setSearchQuery(e.target.value); }; - const handleCompleteRegistration = () => { - if (selectedParticipants.length === 0) { - alert('참여자를 선택해주세요.'); - return; + const handleUserSelect = (user) => { + const isSelected = selectedParticipants.some(p => p.id === user.id); + + if (isSelected) { + // 이미 선택된 사용자라면 제거 + setSelectedParticipants(prev => prev.filter(p => p.id !== user.id)); + } else { + // 새로운 사용자라면 추가 + setSelectedParticipants(prev => [...prev, user]); } + }; - // 전시 등록 페이지로 돌아가면서 참여자 정보 전달 - navigate('/exhibition/upload', { - state: { - exhibitionData: location.state?.exhibitionData, - participants: selectedParticipants - } - }); + const handleComplete = () => { + // 성공 모달 표시 + setShowSuccessModal(true); + + // 2초 후 화면 전환 + setTimeout(() => { + // 전시 업로드 페이지로 돌아가면서 선택된 참여자 정보 전달 + navigate('/exhibition/upload', { + state: { + participants: selectedParticipants, + exhibitionData + } + }); + }, 2000); }; - const isParticipantSelected = (participantId) => { - return selectedParticipants.find(p => p.id === participantId); + const isUserSelected = (userId) => { + return selectedParticipants.some(p => p.id === userId); }; return ( @@ -96,83 +86,110 @@ export default function ExhibitionParticipantPage() {
-

전시 참여자 등록하기

+

+ {selectedParticipants.length > 0 ? '전시 참여자 등록됨' : '전시 참여자 등록하기'} +

- {/* 검색 입력창 */} + {/* 검색창 */}
-
+
- search +
+ 검색 +
- {/* 검색 결과가 있을 때만 표시 */} - {searchResults.length > 0 && ( - <> -

- {selectedParticipants.length > 0 - ? "프로필을 누르면 등록이 취소됩니다" - : "프로필을 누르면 등록이 완료됩니다" - } -

- - {/* 참여자 목록 */} -
- {searchResults.map((participant) => ( -
handleParticipantToggle(participant)} - > + {/* 안내 메시지 */} + {isSearching && searchResults.length > 0 && ( +

+ {selectedParticipants.length === 0 + ? '프로필을 누르면 등록이 완료됩니다' + : `프로필을 누르면 등록이 취소됩니다 (${selectedParticipants.length})` + } +

+ )} + + {/* 검색 결과 목록 */} + {isSearching && searchResults.length > 0 && ( +
+ {searchResults.map((user) => ( +
handleUserSelect(user)} + > +
- {participant.profileImage ? ( - {participant.name} + {user.profileImage ? ( + 프로필 ) : ( -
+
)}
-
-

- {participant.name} @{participant.username} -

+
+ {user.displayName} + @{user.username}
- ))} -
- +
+ ))} +
+ )} + + {/* 선택된 참여자 목록 */} + {selectedParticipants.length > 0 && ( +
+

선택된 참여자

+ {selectedParticipants.map((user) => ( +
handleUserSelect(user)} + > +
+
+ {user.profileImage ? ( + 프로필 + ) : ( +
+ )} +
+
+ {user.displayName} + @{user.username} +
+
+
+ ))} +
)} - {/* 하단 버튼 */} + {/* 등록 완료 버튼 */} - {/* 알림 배너 */} - {showNotification && ( -
- 참여자가 등록 요청이 전송되었습니다 + {/* 성공 모달 */} + {showSuccessModal && ( +
+
+ 참여자 등록 요청이 전송되었습니다 +
)}
diff --git a/src/components/museum/pages/ExhibitionUploadPage.jsx b/src/components/museum/pages/ExhibitionUploadPage.jsx index f0d8a0a..9cb13e5 100644 --- a/src/components/museum/pages/ExhibitionUploadPage.jsx +++ b/src/components/museum/pages/ExhibitionUploadPage.jsx @@ -212,20 +212,21 @@ export default function ExhibitionUploadPage() {
{/* 헤더 */}
- -

전시 등록하기

+
+ +

전시 등록하기

+
-
-
+
{/* 전시 썸네일 및 작품 등록 */}

전시 작품 등록

-
+
{/* 전시 썸네일 */} -
+
{thumbnail ? (
@@ -335,16 +336,10 @@ export default function ExhibitionUploadPage() { } })} > - 전시 참여자 등록하기 - {location.state?.participants && ( - - ✓ {location.state.participants.length}명 등록됨 - - )} + {location.state?.participants ? '전시 참여자 등록됨' : '전시 참여자 등록하기'}
-
{/* 전시 작품 등록 모달 */} { + navigate('/exhibition/shared-library-selection'); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setSharedLibraryData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleThumbnailChange = (e) => { + const file = e.target.files[0]; + if (file) { + setThumbnail(file); + } + }; + + // 공유 라이브러리 작품 등록 모달 열기 + const openArtworkModal = (index) => { + setCurrentArtworkIndex(index); + setIsChangeMode(false); + setIsArtworkModalOpen(true); + }; + + // 공유 라이브러리 작품 변경 모달 열기 + const openChangeArtworkModal = (index) => { + setCurrentArtworkIndex(index); + setIsChangeMode(true); + setIsArtworkModalOpen(true); + }; + + // 새 작품 등록 처리 + const handleNewArtwork = (file) => { + if (currentArtworkIndex === -1) { + // 썸네일인 경우 + setThumbnail(file); + } else { + // 작품인 경우 + if (isChangeMode) { + // 변경 모드: 해당 인덱스의 작품만 수정 + updateArtwork(currentArtworkIndex, file); + } else { + // 새로 추가 모드: 작품 추가 + addArtwork(file); + } + } + }; + + // 작품 라이브러리에서 가져오기 처리 + const handleLoadFromLibrary = () => { + // 작품 라이브러리 페이지로 이동 + navigate('/artwork/library', { + state: { + fromSharedLibrary: true, + currentArtworkIndex, + isChangeMode + } + }); + }; + + // 작품 제거 처리 + const removeArtwork = (index) => { + removeArtworkFromStore(index); + }; + + // 공유 라이브러리 작품 슬라이드 렌더링 + const renderArtworkSlides = () => { + const slides = []; + + // 기존 작품들 렌더링 + artworks.forEach((artwork, index) => { + slides.push( +
+
+ {artwork ? ( +
+ {`작품 + + +
+ ) : ( +
openArtworkModal(index)} + style={{ cursor: 'pointer' }} + > + plus +

+ 공유 라이브러리 작품을 등록해주세요
+ (선택) +

+
+ )} +
+
+ ); + }); + + // 새 작품 추가 버튼 (최대 10개) + if (artworks.length < 10) { + slides.push( +
+
+
openArtworkModal(artworks.length)} + style={{ cursor: 'pointer' }} + > + plus +

+ 공유 라이브러리 작품을 등록해주세요
+ (선택) +

+
+
+
+ ); + } + + return slides; + }; + + return ( +
+ {/* 헤더 */} +
+
+ +

공유 라이브러리

+
+
+ +
+ {/* 공유 라이브러리 썸네일 및 작품 등록 */} +
+

공유 라이브러리 작품 등록

+
+ {/* 공유 라이브러리 썸네일 */} +
+
+ {thumbnail ? ( +
+ 공유 라이브러리 썸네일 + +
+ ) : ( +
openArtworkModal(-1)} // -1은 썸네일을 의미 + style={{ cursor: 'pointer' }} + > + camera +

+ 공유 라이브러리 썸네일을 등록해주세요
+ (필수) +

+
+ )} +
+
+ + {/* 공유 라이브러리 작품 슬라이드 (동적으로 생성) */} + {renderArtworkSlides()} +
+
+ + {/* 공유 라이브러리명 입력 */} +
+ +
+ + {/* 공유 라이브러리 소개 입력 */} +
+