@@ -51,7 +51,7 @@ export default function UserEditModal({ isOpen, onClose }) {
-
📧
+
이메일
-
📷
+
인스타그램
-
📧
+
이메일 {user.email}
-
📷
+
인스타그램 @{user.instagram || user.nickname}
diff --git a/src/components/user/userEdit.module.css b/src/components/user/userEdit.module.css
index b619af7..f133a56 100644
--- a/src/components/user/userEdit.module.css
+++ b/src/components/user/userEdit.module.css
@@ -237,10 +237,10 @@
.emailInputContainer {
display: flex;
align-items: center;
- gap: 4px;
width: 100%;
max-width: 100%;
- overflow: hidden;
+ flex-wrap: nowrap;
+ min-width: 0;
}
.emailUsernameInput {
@@ -256,7 +256,8 @@
line-height: 22px;
box-sizing: border-box;
flex: 1;
- min-width: 80px;
+ min-width: 60px;
+ max-width: 110px;
outline: none;
}
@@ -272,6 +273,7 @@
letter-spacing: -0.35px;
line-height: 22px;
padding: 0 4px;
+ flex-shrink: 0;
}
.emailDomainSelect {
@@ -288,6 +290,7 @@
box-sizing: border-box;
flex: 1;
min-width: 90px;
+ max-width: 150px;
cursor: pointer;
}
@@ -305,6 +308,7 @@
box-sizing: border-box;
flex: 1;
min-width: 90px;
+ max-width: 150px;
outline: none;
}
diff --git a/src/dummy.js b/src/dummy.js
index 72e349c..6e26333 100644
--- a/src/dummy.js
+++ b/src/dummy.js
@@ -6,7 +6,7 @@ export const artworks = [
artist: "김민수",
year: 2023,
description: "현대 도시 풍경을 따뜻한 색조로 표현한 작품",
- image: "https://picsum.photos/400/300?random=1",
+ image: "/artwork1.png",
price: "150만원",
position: [-8, 2.5, -7.8], // 뒷벽 왼쪽
},
@@ -16,7 +16,7 @@ export const artworks = [
artist: "이영희",
year: 2023,
description: "자연 속에서 찾은 평온함을 담은 추상화",
- image: "https://picsum.photos/400/300?random=2",
+ image: "/artwork2.png",
price: "200만원",
position: [0, 2.5, -7.8], // 뒷벽 중앙
},
@@ -26,7 +26,7 @@ export const artworks = [
artist: "박철수",
year: 2022,
description: "과거와 현재를 잇는 시간의 흐름을 표현",
- image: "https://picsum.photos/400/300?random=3",
+ image: "/artwork3.png",
price: "180만원",
position: [8, 2.5, -7.8], // 뒷벽 오른쪽
},
@@ -36,7 +36,7 @@ export const artworks = [
artist: "최수진",
year: 2023,
description: "푸른 바다의 무한함과 꿈을 그린 작품",
- image: "https://picsum.photos/400/300?random=4",
+ image: "/artwork1.png",
price: "220만원",
position: [-17.3, 2.5, -4], // 왼쪽 벽 뒤쪽
},
@@ -46,7 +46,7 @@ export const artworks = [
artist: "정다은",
year: 2023,
description: "따스한 봄날의 감성을 담은 서정적 작품",
- image: "https://picsum.photos/400/300?random=5",
+ image: "/artwork2.png",
price: "160만원",
position: [17.3, 2.5, -4], // 오른쪽 벽 뒤쪽
},
@@ -56,7 +56,7 @@ export const artworks = [
artist: "한지민",
year: 2022,
description: "현대인의 고독과 소통에 대한 성찰",
- image: "https://picsum.photos/400/300?random=6",
+ image: "/artwork3.png",
price: "190만원",
position: [-17.3, 2.5, 4], // 왼쪽 벽 앞쪽
},
@@ -66,7 +66,7 @@ export const artworks = [
artist: "조영수",
year: 2023,
description: "세월이 남긴 흔적들의 아름다움을 포착한 작품",
- image: "https://picsum.photos/400/300?random=7",
+ image: "/artwork1.png",
price: "170만원",
position: [17.3, 2.5, 4], // 오른쪽 벽 앞쪽
},
@@ -76,7 +76,7 @@ export const artworks = [
artist: "이서연",
year: 2023,
description: "희망찬 새벽을 맞이하는 마음을 그린 작품",
- image: "https://picsum.photos/400/300?random=8",
+ image: "/artwork2.png",
price: "210만원",
position: [-6, 2.5, 7.8], // 앞쪽 벽 왼쪽
},
@@ -86,7 +86,7 @@ export const artworks = [
artist: "김태현",
year: 2022,
description: "바쁜 도시 생활 속에서 발견한 고유한 리듬감",
- image: "https://picsum.photos/400/300?random=9",
+ image: "/artwork3.png",
price: "185만원",
position: [6, 2.5, 7.8], // 앞쪽 벽 오른쪽
},
diff --git a/src/pages/MuseumPage.jsx b/src/pages/MuseumPage.jsx
index 5f90d63..fe9eab9 100644
--- a/src/pages/MuseumPage.jsx
+++ b/src/pages/MuseumPage.jsx
@@ -8,24 +8,44 @@ import InvitationSection from "@museum/components/museum/InvitationSection";
import BackToTopButton from "@/components/common/BackToTopButton";
import AppFooter from "@/components/footer/AppFooter";
import useUserStore from "@/stores/userStore";
+import { fetchMyArtworks } from "@/apis/artwork";
import styles from "@museum/components/museum/museum.module.css";
import commonStyles from "@museum/components/museum/common.module.css";
// 이미지 import
-import artwork1 from "@/assets/museum/큰사진3.png";
-import artwork2 from "@/assets/museum/큰사진4.png";
-import artwork3 from "@/assets/museum/큰사진5.png";
import exhibition1 from "@/assets/museum/큰사진1.png";
import exhibition2 from "@/assets/museum/큰사진2.png";
-
export default function MuseumPage() {
// Zustand에서 사용자 정보 가져오기
const { user, subscription, invitation } = useUserStore();
+ // 작품 데이터 상태
+ const [artworks, setArtworks] = useState([]);
+
// 스크롤 상태 관리
const [isScrolled, setIsScrolled] = useState(false);
+ // 작품 데이터 로드
+ useEffect(() => {
+ const loadArtworks = async () => {
+ try {
+ console.log('작품 데이터 로드 시작...');
+ const response = await fetchMyArtworks(true, 0, 10); // 등록 완료된 작품 10개
+ console.log('API 응답:', response);
+ console.log('응답 데이터:', response.data);
+ console.log('작품 목록:', response.content);
+
+ setArtworks(response.content || []);
+ console.log('설정된 작품 목록:', response.content || []);
+ } catch (error) {
+ console.error('작품 로드 오류:', error);
+ }
+ };
+
+ loadArtworks();
+ }, []);
+
// 스크롤 이벤트 처리
useEffect(() => {
const handleScroll = () => {
@@ -37,26 +57,6 @@ export default function MuseumPage() {
return () => window.removeEventListener('scroll', handleScroll);
}, []);
- const artworks = [
- { id: 1, image: artwork1, title: "정원에서의 오후" },
- { id: 2, image: artwork2, title: "에트르타 절벽" },
- { id: 3, image: artwork3, title: "바다 풍경" },
- { id: 4, image: artwork1, title: "보르디게라의 정원" },
- { id: 5, image: artwork2, title: "강가의 휴식" },
- { id: 6, image: artwork3, title: "모네의 정원 시리즈 1" },
- { id: 7, image: artwork1, title: "바위와 바다" },
- { id: 8, image: artwork2, title: "푸른 바다" },
- { id: 9, image: artwork3, title: "나무와 빛" },
- { id: 10, image: artwork1, title: "물가의 평온" },
- { id: 11, image: artwork2, title: "인상파 풍경 1" },
- { id: 12, image: artwork3, title: "인상파 풍경 2" },
- { id: 13, image: artwork1, title: "인상파 풍경 3" },
- { id: 14, image: artwork2, title: "인상파 풍경 4" },
- { id: 15, image: artwork3, title: "인상파 풍경 5" },
- { id: 16, image: artwork1, title: "자연의 순간 1" },
- { id: 17, image: artwork2, title: "자연의 순간 2" },
- ];
-
const exhibitions = [
{
id: 1,
diff --git a/src/routers/index.jsx b/src/routers/index.jsx
index aa9f7b7..eb41022 100644
--- a/src/routers/index.jsx
+++ b/src/routers/index.jsx
@@ -2,7 +2,6 @@ import { createBrowserRouter } from "react-router-dom";
import FeedPage from "../pages/FeedPage";
import MuseumPage from "../pages/MuseumPage";
import MyPage from "../pages/MyPage";
-import MyTypePage from "../pages/MyTypePage";
import UserProfileDetailPage from "@/components/user/UserProfileDetailPage";
import UserEditPage from "@/components/user/UserEditPage";
import ContactEditPage from "@/components/user/ContactEditPage";
From a604b3bca119a7d43a4561bf4252ae48dd135974 Mon Sep 17 00:00:00 2001
From: wnsgur393 <2021301022@skuniv.ac.kr>
Date: Sun, 24 Aug 2025 23:07:49 +0900
Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20jhpart=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/museum/artwork.js | 320 ++++++++
src/apis/{exhibition => museum}/exhibition.js | 42 +
src/apis/user/user.js | 178 ++++
src/components/Gallery3D.jsx | 4 +-
.../museum/components/artwork/ArtworkCard.jsx | 70 +-
.../components/artwork/ArtworkFilter.jsx | 22 +-
.../museum/components/artwork/ArtworkList.jsx | 115 +--
.../components/artwork/DraftArtworkList.jsx | 48 +-
.../components/artwork/artworkCard.module.css | 24 +
.../artwork/artworkFilter.module.css | 33 -
.../exhibition/ExhibitionArtworkModal.jsx | 33 +-
.../components/exhibition/ExhibitionCard.jsx | 27 +-
.../exhibition/ExhibitionDatePicker.jsx | 131 +--
.../components/exhibition/ExhibitionList.jsx | 25 +-
.../exhibition/exhibitionCard.module.css | 151 ++--
.../exhibition/exhibitionFilter.module.css | 93 +--
.../exhibition/exhibitionList.module.css | 40 +-
.../components/museum/ArtworkSection.jsx | 40 +-
.../components/museum/ExhibitionSection.jsx | 41 +-
.../components/museum/MuseumProfile.jsx | 6 +-
.../museum/museumProfile.module.css | 1 +
.../museum/pages/ArtworkLibraryPage.jsx | 96 ++-
.../museum/pages/ArtworkUploadPage.jsx | 362 ++++++---
.../museum/pages/DraftArtworkPage.jsx | 55 +-
.../museum/pages/ExhibitionInvitationPage.jsx | 3 -
.../pages/ExhibitionParticipantPage.jsx | 84 +-
.../museum/pages/ExhibitionUploadPage.jsx | 762 +++++++++++++-----
src/components/museum/pages/MyArtworkPage.jsx | 59 +-
.../museum/pages/MyExhibitionPage.jsx | 2 +-
.../museum/pages/OfflineLocationPage.jsx | 452 ++---------
.../museum/pages/SharedLibraryEntryPage.jsx | 12 -
.../pages/SharedLibrarySelectionPage.jsx | 8 -
.../pages/offlineLocationPage.module.css | 13 +-
src/components/museum/services/artworkApi.js | 73 --
.../museum/services/artworkDraftStore.js | 110 ---
.../museum/services/artworkStore.js | 142 ----
.../museum/services/exhibitionPhotoStore.js | 63 --
src/components/user/ContactEditPage.jsx | 144 +++-
src/components/user/UserEditModal.jsx | 4 +-
src/components/user/UserEditPage.jsx | 235 +++++-
src/components/user/UserProfileDetailPage.jsx | 77 +-
src/components/user/UserProfileHeader.jsx | 38 +-
src/components/user/UserProfileModal.jsx | 20 +-
src/components/user/userEdit.module.css | 45 ++
.../user/userProfileDetail.module.css | 5 +-
src/pages/ExhibitionDetailPage.jsx | 2 +-
src/pages/MuseumPage.jsx | 115 ++-
src/stores/userStore.js | 83 +-
vite.config.js | 4 +-
49 files changed, 2682 insertions(+), 1830 deletions(-)
create mode 100644 src/apis/museum/artwork.js
rename src/apis/{exhibition => museum}/exhibition.js (83%)
create mode 100644 src/apis/user/user.js
delete mode 100644 src/components/museum/services/artworkApi.js
delete mode 100644 src/components/museum/services/artworkDraftStore.js
delete mode 100644 src/components/museum/services/artworkStore.js
delete mode 100644 src/components/museum/services/exhibitionPhotoStore.js
diff --git a/src/apis/museum/artwork.js b/src/apis/museum/artwork.js
new file mode 100644
index 0000000..ca42cee
--- /dev/null
+++ b/src/apis/museum/artwork.js
@@ -0,0 +1,320 @@
+import { APIService } from '../axios.js';
+import { useState, useEffect, useCallback } from 'react';
+
+/**
+ * 내 작품 리스트 조회 API
+ * @param {Object} params - 쿼리 파라미터
+ * @param {boolean} params.applicated - 등록 신청 여부
+ * @param {number} params.pageNum - 페이지 번호 (기본값: 1)
+ * @param {number} params.pageSize - 페이지 크기 (기본값: 10)
+ * @returns {Promise} API 응답 데이터
+ */
+export const getMyPieces = async (params = {}) => {
+ try {
+ const { applicated = true, pageNum = 1, pageSize = 3 } = params;
+
+ const response = await APIService.private.get('/api/pieces/my-page', {
+ params: {
+ applicated,
+ pageNum,
+ pageSize
+ }
+ });
+
+ return response;
+ } catch (error) {
+ console.error('내 작품 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 내 작품 상세 정보 조회 API
+ * @param {string} pieceId - 작품 ID
+ * @returns {Promise} API 응답 데이터
+ */
+export const getMyPieceDetail = async (pieceId) => {
+ try {
+ const response = await APIService.private.get(`/api/pieces/${pieceId}`);
+ return response;
+ } catch (error) {
+ console.error('내 작품 상세 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 작품 등록 API
+ * @param {Object} formData - 작품 등록 데이터
+ * @param {string} formData.title - 작품명
+ * @param {string} formData.description - 작품 소개
+ * @param {File} formData.mainImage - 메인 이미지 파일
+ * @param {File[]} formData.detailImages - 디테일 이미지 파일들
+ * @param {string} saveStatus - 저장 상태 (DRAFT 또는 APPLICATION)
+ * @returns {Promise} API 응답 데이터
+ */
+export const createPiece = async (formData, saveStatus = 'APPLICATION') => {
+ try {
+ const data = new FormData();
+
+ // 작품 정보를 JSON으로 변환하여 data 필드에 추가
+ const pieceData = {
+ title: formData.title,
+ description: formData.description,
+ isPurchasable: true // 사용자 요구사항: 항상 true로 설정
+ };
+
+ data.append('data', JSON.stringify(pieceData));
+
+ // 메인 이미지 추가
+ if (formData.mainImage) {
+ data.append('mainImage', formData.mainImage);
+ }
+
+ // 디테일 이미지들 추가
+ if (formData.detailImages && formData.detailImages.length > 0) {
+ formData.detailImages.forEach((image, index) => {
+ if (image) {
+ data.append('detailImages', image);
+ }
+ });
+ }
+
+ const response = await APIService.private.post('/api/pieces', data, {
+ params: { saveStatus },
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+
+ return response;
+ } catch (error) {
+ console.error('작품 등록 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 작품 임시저장 API
+ * @param {Object} formData - 작품 임시저장 데이터
+ * @returns {Promise} API 응답 데이터
+ */
+export const saveDraftPiece = async (formData) => {
+ try {
+ const response = await createPiece(formData, 'DRAFT');
+ return response;
+ } catch (error) {
+ console.error('작품 임시저장 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 여러 작품 삭제 API
+ * @param {number[]} pieceIds - 삭제할 작품 ID 배열
+ * @returns {Promise} API 응답 데이터
+ */
+export const deletePieces = async (pieceIds) => {
+ try {
+ const response = await APIService.private.delete('/api/pieces', {
+ params: {
+ pieceIds: pieceIds.join(',')
+ }
+ });
+ return response;
+ } catch (error) {
+ console.error('작품들 삭제 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 임시저장된 작품 개수 조회 API
+ * @returns {Promise} API 응답 데이터
+ */
+export const getPieceDraftCount = async () => {
+ try {
+ const response = await APIService.private.get('/api/pieces/draft-count');
+ return response;
+ } catch (error) {
+ console.error('임시저장 작품 개수 조회 실패:', error);
+ throw error;
+ }
+};
+
+
+
+/**
+ * 무한스크롤을 위한 작품 목록 관리 훅
+ * @param {Object} params - 쿼리 파라미터
+ * @param {boolean} params.applicated - 등록 신청 여부 (기본값: true)
+ * @param {number} params.pageSize - 페이지 크기 (기본값: 3)
+ * @returns {object} 작품 데이터와 상태 관리 함수들
+ */
+export const useInfinitePieces = (params = {}) => {
+ const { applicated = true, pageSize = 3 } = params;
+
+ const [pieces, setPieces] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [hasMore, setHasMore] = useState(true);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1); // 1부터 시작
+ const [totalElements, setTotalElements] = useState(0);
+
+ // 작품 목록 초기화
+ const resetPieces = useCallback(() => {
+ setPieces([]);
+ setCurrentPage(1); // 1부터 시작
+ setHasMore(true);
+ setError(null);
+ setTotalElements(0);
+ }, []);
+
+ // applicated 변경 시 초기화
+ useEffect(() => {
+ resetPieces();
+ }, [applicated, resetPieces]);
+
+ // 첫 페이지 자동 로드
+ useEffect(() => {
+ // applicated가 변경되거나 pieces가 비어있을 때만 실행
+ if (pieces.length === 0 && !loading) {
+ const loadFirstPage = async () => {
+ if (loading || !hasMore) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // 첫 페이지(1)로 API 호출
+ const response = await getMyPieces({
+ applicated,
+ pageNum: 1,
+ pageSize
+ });
+
+ // API 응답 구조: { content: [], last: boolean, totalElements: number }
+ const apiResponse = response.data || response;
+ const newPieces = apiResponse.content || [];
+ const isLastPage = apiResponse.last; // last가 true면 마지막 페이지
+ const totalElements = apiResponse.totalElements || 0;
+
+ setPieces(newPieces);
+ setHasMore(!isLastPage); // last가 true면 더 이상 로드하지 않음
+ setTotalElements(totalElements);
+ setCurrentPage(2); // 다음 페이지는 2
+ } catch (err) {
+ setError(err);
+ console.error('작품 로드 실패:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadFirstPage();
+ }
+ }, [applicated, pieces.length, loading, hasMore, pageSize]);
+
+ // 다음 페이지 작품 로드
+ const loadMorePieces = useCallback(async () => {
+ if (loading || !hasMore) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // 현재 페이지 번호로 API 호출
+ const response = await getMyPieces({
+ applicated,
+ pageNum: currentPage,
+ pageSize
+ });
+
+ // API 응답 구조: { content: [], last: boolean, totalElements: number }
+ const apiResponse = response.data || response;
+ const newPieces = apiResponse.content || [];
+ const isLastPage = apiResponse.last; // last가 true면 마지막 페이지
+ const totalElements = apiResponse.totalElements || 0;
+
+ setPieces(prev => [...prev, ...newPieces]);
+ setHasMore(!isLastPage); // last가 true면 더 이상 로드하지 않음
+ setTotalElements(totalElements);
+
+ // 다음 페이지 번호로 업데이트 (현재 페이지 로드 완료 후)
+ setCurrentPage(prev => prev + 1);
+ } catch (err) {
+ setError(err);
+ console.error('작품 로드 실패:', err);
+ } finally {
+ setLoading(false);
+ }
+ }, [applicated, pageSize, currentPage, loading, hasMore]);
+
+ return {
+ pieces,
+ loading,
+ hasMore,
+ error,
+ loadMorePieces,
+ resetPieces,
+ totalElements,
+ currentPage
+ };
+};
+
+/**
+ * 작품 이미지 URL을 여러 가능한 필드에서 찾는 유틸리티 함수
+ * @param {Object} artwork - 작품 객체
+ * @returns {string|null} 이미지 URL 또는 null
+ */
+export const getImageUrl = (artwork) => {
+ if (!artwork) return null;
+
+ // API에서 받아오는 imageUrl 필드를 우선적으로 확인
+ const imageUrl = artwork.imageUrl ||
+ artwork.mainImageUrl ||
+ artwork.thumbnailUrl ||
+ artwork.image ||
+ artwork.mainImage;
+
+ return imageUrl;
+};
+
+/**
+ * 작품 상태에 따른 스타일 클래스명을 반환하는 유틸리티 함수
+ * @param {string} status - 작품 상태
+ * @returns {string} CSS 클래스명
+ */
+export const getStatusStyle = (status) => {
+ switch (status) {
+ case '전시 중':
+ return 'statusExhibiting';
+ case '미승인':
+ return 'statusRejected';
+ case '승인 대기':
+ return 'statusPending';
+ default:
+ return 'statusDefault';
+ }
+};
+
+/**
+ * 작품 제목을 안전하게 표시하는 유틸리티 함수
+ * @param {string} title - 작품 제목
+ * @param {string} fallback - 기본값
+ * @returns {string} 표시할 제목
+ */
+export const getArtworkTitle = (title, fallback = '제목 없음') => {
+ return title && title.trim() ? title.trim() : fallback;
+};
+
+/**
+ * 작품 설명을 안전하게 표시하는 유틸리티 함수
+ * @param {string} description - 작품 설명
+ * @param {string} fallback - 기본값
+ * @returns {string} 표시할 설명
+ */
+export const getArtworkDescription = (description, fallback = '설명 없음') => {
+ return description && description.trim() ? description.trim() : fallback;
+};
+
diff --git a/src/apis/exhibition/exhibition.js b/src/apis/museum/exhibition.js
similarity index 83%
rename from src/apis/exhibition/exhibition.js
rename to src/apis/museum/exhibition.js
index 5ca331a..88bb5a3 100644
--- a/src/apis/exhibition/exhibition.js
+++ b/src/apis/museum/exhibition.js
@@ -1,6 +1,30 @@
import { useState, useEffect, useCallback } from 'react';
import APIService from '../axios';
+/**
+ * 전시 등록 API
+ * @param {Object} exhibitionData - 전시 등록 데이터
+ * @param {number[]} exhibitionData.pieceIdList - 작품 ID 목록
+ * @param {string} exhibitionData.endDate - 종료일 (YYYY-MM-DD)
+ * @param {number[]} exhibitionData.participantIdList - 참여자 ID 목록
+ * @param {string} exhibitionData.startDate - 시작일 (YYYY-MM-DD)
+ * @param {string} exhibitionData.address - 주소
+ * @param {string} exhibitionData.title - 전시 제목
+ * @param {string} exhibitionData.offlineDescription - 오프라인 전시 설명
+ * @param {string} exhibitionData.description - 전시 설명
+ * @param {string} exhibitionData.addressName - 주소명
+ * @returns {Promise} 전시 등록 결과
+ */
+export const createExhibition = async (exhibitionData) => {
+ try {
+ const response = await APIService.private.post('/api/exhibitions', exhibitionData);
+ return response;
+ } catch (error) {
+ console.error('전시 등록 실패:', error);
+ throw error;
+ }
+};
+
/**
* 전시 상세 정보 조회 API
* @param {number} exhibitionId - 전시 ID
@@ -90,6 +114,24 @@ export const getExhibitionReviewsPreview = async (exhibitionId) => {
}
};
+/**
+ * 내 전시 목록 조회 API
+ * @param {Object} params - 페이지네이션 파라미터
+ * @param {number} params.pageNum - 페이지 번호
+ * @param {number} params.pageSize - 페이지 크기
+ * @param {boolean} params.fillAll - 모든 정보 채우기 여부
+ * @returns {Promise} 내 전시 목록
+ */
+export const getMyExhibitions = async (params = { pageNum: 1, pageSize: 3, fillAll: true }) => {
+ try {
+ const response = await APIService.private.get('/api/exhibitions/my-page', { params });
+ return response;
+ } catch (error) {
+ console.error('내 전시 목록 조회 실패:', error);
+ throw error;
+ }
+};
+
/**
* 전시 상세 정보를 위한 커스텀 훅
* @param {number} exhibitionId - 전시 ID
diff --git a/src/apis/user/user.js b/src/apis/user/user.js
new file mode 100644
index 0000000..b3b6dd2
--- /dev/null
+++ b/src/apis/user/user.js
@@ -0,0 +1,178 @@
+import { APIService } from '../axios.js';
+
+/**
+ * 현재 로그인한 사용자 정보 조회 API
+ * @returns {Promise} API 응답 데이터
+ */
+export const getCurrentUser = async () => {
+ try {
+ const response = await APIService.private.get('/api/users');
+ return response;
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 특정 사용자 정보 조회 API
+ * @param {string} userId - 사용자 ID
+ * @returns {Promise} API 응답 데이터
+ */
+export const getUserById = async (userId) => {
+ try {
+ const response = await APIService.private.get(`/api/users/${userId}/creator`);
+ return response;
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 프로필 수정 API
+ * @param {Object} userData - 수정할 사용자 데이터
+ * @param {string} userData.nickname - 닉네임
+ * @param {string} userData.introduction - 자기소개
+ * @param {string} userData.code - 코드
+ * @returns {Promise} API 응답 데이터
+ */
+export const updateUserProfile = async (userData) => {
+ try {
+ // FormData 객체 생성
+ const formData = new FormData();
+
+ // request라는 이름으로 JSON 데이터를 추가
+ const requestData = {
+ nickname: userData.nickname,
+ code: userData.code,
+ introduction: userData.introduction
+ };
+
+ formData.append('request', new Blob([JSON.stringify(requestData)], {
+ type: 'application/json'
+ }));
+
+ const response = await APIService.private.put('/api/users', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ return response;
+ } catch (error) {
+ console.error('사용자 프로필 수정 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 프로필 이미지 수정 API
+ * @param {File} profileImage - 프로필 이미지 파일
+ * @returns {Promise} API 응답 데이터
+ */
+export const updateUserProfileImage = async (profileImage) => {
+ try {
+ const formData = new FormData();
+ formData.append('profileImage', profileImage); // profileImageUrl -> profileImage로 변경
+
+ const response = await APIService.private.put('/api/users/profile-image', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ return response;
+ } catch (error) {
+ console.error('프로필 이미지 수정 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 계정 삭제 API
+ * @returns {Promise} API 응답 데이터
+ */
+export const deleteUserAccount = async () => {
+ try {
+ const response = await APIService.private.delete('/api/users');
+ return response;
+ } catch (error) {
+ console.error('사용자 계정 삭제 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 코드로 프로필 조회 API
+ * @param {string} userCode - 사용자 코드 (예: @username)
+ * @returns {Promise} API 응답 데이터
+ */
+export const getUserProfilesByCode = async (userCode) => {
+ try {
+ const response = await APIService.private.get(`/api/users/search?keyword=${userCode}`);
+ return response;
+ } catch (error) {
+ console.error('사용자 프로필 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 연락 정보 등록 상태 조회 API
+ * @returns {Promise} API 응답 데이터
+ */
+export const getContactStatus = async () => {
+ try {
+ const response = await APIService.private.get('/api/users/contact/status');
+ return response;
+ } catch (error) {
+ console.error('연락 정보 상태 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 연락 정보 수정 API
+ * @param {Object} contactData - 수정할 연락 정보 데이터
+ * @param {string} contactData.email
+ * @param {string} contactData.instagram
+ * @returns {Promise} API 응답 데이터
+ */
+export const updateContact = async (contactData) => {
+ try {
+ const response = await APIService.private.put('/api/users/contact', contactData);
+ return response;
+ } catch (error) {
+ console.error('연락 정보 수정 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 연락 정보 조회 API
+ * @param {string} userId - 사용자 ID
+ * @returns {Promise} API 응답 데이터
+ */
+export const getUserContact = async (userId) => {
+ try {
+ const response = await APIService.private.get(`/api/users/${userId}/contact`);
+ return response;
+ } catch (error) {
+ console.error('사용자 연락 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+/**
+ * 사용자 코드 중복 확인 API
+ * @param {string} code - 확인할 사용자 코드
+ * @returns {Promise} API 응답 데이터
+ */
+export const checkUserCode = async (code) => {
+ try {
+ const response = await APIService.private.get(`/api/users/check-code?code=${code}`);
+ return response;
+ } catch (error) {
+ console.error('사용자 코드 중복 확인 실패:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/components/Gallery3D.jsx b/src/components/Gallery3D.jsx
index 5afd992..1ae6c53 100644
--- a/src/components/Gallery3D.jsx
+++ b/src/components/Gallery3D.jsx
@@ -3,8 +3,8 @@ import { Environment } from "@react-three/drei";
import { Suspense, useState } from "react";
import Exhibition from "./Exhibition";
import CameraController from "./CameraController";
-import ResetCameraButton from "./common/ResetCameraButton";
-import ControlsInfoModal from "./common/ControlsInfoModal";
+import ResetCameraButton from "./ResetCameraButton";
+import ControlsInfoModal from "./ControlsInfoModal";
import "./Gallery3D.css";
function LoadingFallback() {
diff --git a/src/components/museum/components/artwork/ArtworkCard.jsx b/src/components/museum/components/artwork/ArtworkCard.jsx
index 2ccf324..d7b9133 100644
--- a/src/components/museum/components/artwork/ArtworkCard.jsx
+++ b/src/components/museum/components/artwork/ArtworkCard.jsx
@@ -1,3 +1,5 @@
+import { useState } from 'react';
+import { getImageUrl, getStatusStyle } from '@apis/museum/artwork';
import styles from './artworkCard.module.css';
import checkIcon from '@/assets/museum/check.png';
@@ -14,11 +16,24 @@ export default function ArtworkCard({
onSelect,
checkboxSize = 'normal' // 'normal' (24x24) 또는 'large' (36x36)
}) {
+ const [imageError, setImageError] = useState(false);
+
+ const imageUrl = getImageUrl(artwork);
+
+ const handleImageError = () => {
+ setImageError(true);
+ };
+
const handleClick = () => {
+ if (onClick) {
+ onClick(artwork);
+ }
+ };
+
+ const handleCheckboxClick = (e) => {
+ e.stopPropagation();
if (isEditMode && onSelect) {
onSelect(artwork);
- } else if (!isEditMode && onClick) {
- onClick(artwork);
}
};
@@ -29,35 +44,31 @@ export default function ArtworkCard({
}
};
- const getStatusStyle = (status) => {
- switch (status) {
- case '전시 중':
- return styles.statusExhibiting;
- case '미승인':
- return styles.statusRejected;
- case '승인 대기':
- return styles.statusPending;
- default:
- return styles.statusDefault;
- }
- };
-
if (layoutMode === 'grid') {
return (
- {showStatus && artwork.status && (
-
- {artwork.status}
+ {!imageUrl || imageError ? (
+
+ 이미지 없음
+
+ ) : null}
+
+ {showStatus && artwork.progressStatus === 'ON_DISPLAY' && (
+
+ 전시 중
)}
{isEditMode && (
-
+
{isSelected && (
)}
@@ -75,16 +86,25 @@ export default function ArtworkCard({
- {showStatus && artwork.status && (
-
- {artwork.status}
+ {!imageUrl || imageError ? (
+
+ 이미지 없음
+
+ ) : null}
+
+ {showStatus && artwork.progressStatus === 'ON_DISPLAY' && (
+
+ 전시 중
)}
{isEditMode && (
-
+
{isSelected && (
)}
diff --git a/src/components/museum/components/artwork/ArtworkFilter.jsx b/src/components/museum/components/artwork/ArtworkFilter.jsx
index e53002a..9734a9b 100644
--- a/src/components/museum/components/artwork/ArtworkFilter.jsx
+++ b/src/components/museum/components/artwork/ArtworkFilter.jsx
@@ -6,9 +6,7 @@ export default function ArtworkFilter({
layoutMode,
onLayoutChange,
searchKeyword,
- onSearchChange,
- applicated,
- onApplicatedChange
+ onSearchChange
}) {
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [localSearchKeyword, setLocalSearchKeyword] = useState(searchKeyword || '');
@@ -37,26 +35,8 @@ export default function ArtworkFilter({
setLocalSearchKeyword(e.target.value);
};
- const handleApplicatedToggle = () => {
- if (onApplicatedChange) {
- onApplicatedChange();
- }
- };
-
return (
- {/* 등록 상태 토글 */}
- {onApplicatedChange && (
-
-
- {applicated ? '등록 신청한 작품' : '임시 저장된 작품'}
-
-
- )}
-
{/* 검색바 */}
{showDraftButton && (
@@ -234,7 +245,7 @@ export default function ArtworkList({
)}
-
{isLibraryMode ? libraryTitle : '내 작품'}
+
{title || (isLibraryMode ? libraryTitle : '내 작품')}
)}
@@ -245,8 +256,6 @@ export default function ArtworkList({
onLayoutChange={handleLayoutChange}
searchKeyword={searchKeyword}
onSearchChange={handleSearchChange}
- applicated={applicated}
- onApplicatedChange={handleApplicatedChange}
/>
{/* 작품 목록 */}
@@ -269,7 +278,7 @@ export default function ArtworkList({
const isLast = index === filteredArtworks.length - 1;
return (
@@ -291,7 +300,7 @@ export default function ArtworkList({
)}
{/* 더 로딩 중 표시 */}
- {isLoadingMore && (
+ {loading && (
더 많은 작품을 불러오는 중...
@@ -305,9 +314,9 @@ export default function ArtworkList({
- {selectedArtworks.size > 0 ? `${selectedArtworks.size}개 삭제하기` : '삭제하기'}
+ {isDeleting ? '삭제 중...' : selectedArtworks.size > 0 ? `${selectedArtworks.size}개 삭제하기` : '삭제하기'}
) : (
diff --git a/src/components/museum/components/artwork/DraftArtworkList.jsx b/src/components/museum/components/artwork/DraftArtworkList.jsx
index 9123387..c6a1d17 100644
--- a/src/components/museum/components/artwork/DraftArtworkList.jsx
+++ b/src/components/museum/components/artwork/DraftArtworkList.jsx
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import ArtworkCard from './ArtworkCard';
import ArtworkFilter from './ArtworkFilter';
-import useArtworkDraftStore from '@museum/services/artworkDraftStore';
import BackToTopButton from '@/components/common/BackToTopButton';
import chevronLeft from '@/assets/museum/chevron-left.png';
import arrowDown from '@/assets/museum/arrow-down.svg';
@@ -15,21 +14,56 @@ export default function DraftArtworkList({
onBack,
onArtworkClick
}) {
- // Zustand 스토어에서 임시저장 데이터 가져오기
- const {
- drafts,
- deleteDraft
- } = useArtworkDraftStore();
-
+ // 로컬 상태로 draft 관리
+ const [drafts, setDrafts] = useState([]);
const [isEditMode, setIsEditMode] = useState(false);
const [showScrollHeader, setShowScrollHeader] = useState(false);
const [selectedDrafts, setSelectedDrafts] = useState(new Set());
const [layoutMode, setLayoutMode] = useState('vertical');
const [searchKeyword, setSearchKeyword] = useState('');
+
const observerRef = useRef();
const loadingRef = useRef();
const headerRef = useRef();
+ // localStorage에서 draft 데이터 로드
+ useEffect(() => {
+ const loadDrafts = () => {
+ try {
+ const savedDraft = localStorage.getItem('artworkDraft');
+ if (savedDraft) {
+ const draftData = JSON.parse(savedDraft);
+ // draft 데이터를 배열 형태로 변환
+ setDrafts([{
+ id: 'draft-1',
+ title: draftData.title || '제목 없음',
+ description: draftData.description || '설명 없음',
+ image: draftData.mainImage ? URL.createObjectURL(draftData.mainImage) : null,
+ status: "임시저장",
+ createdAt: new Date().toLocaleDateString('ko-KR'),
+ isExhibiting: false
+ }]);
+ } else {
+ setDrafts([]);
+ }
+ } catch (error) {
+ console.error('Draft 로드 실패:', error);
+ setDrafts([]);
+ }
+ };
+
+ loadDrafts();
+ }, []);
+
+ // draft 삭제 함수
+ const deleteDraft = (draftId) => {
+ if (draftId === 'draft-1') {
+ // localStorage에서 draft 제거
+ localStorage.removeItem('artworkDraft');
+ setDrafts([]);
+ }
+ };
+
// 임시저장 데이터를 ArtworkCard에서 사용할 수 있는 형태로 변환
const convertedDrafts = drafts.map(draft => ({
id: draft.id,
diff --git a/src/components/museum/components/artwork/artworkCard.module.css b/src/components/museum/components/artwork/artworkCard.module.css
index d63a818..e9cf68a 100644
--- a/src/components/museum/components/artwork/artworkCard.module.css
+++ b/src/components/museum/components/artwork/artworkCard.module.css
@@ -72,6 +72,11 @@
right: 12px;
}
+.gridImage .statusBadge {
+ top: 8px;
+ right: 8px;
+}
+
.dateDisplay {
writing-mode: vertical-rl;
text-orientation: mixed;
@@ -218,4 +223,23 @@
cursor: not-allowed;
}
+.noImagePlaceholder {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: #f0f0f0;
+ color: #999;
+ font-family: 'Pretendard', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+ border: 1px solid #ddd;
+}
+
diff --git a/src/components/museum/components/artwork/artworkFilter.module.css b/src/components/museum/components/artwork/artworkFilter.module.css
index 9819e61..9e22646 100644
--- a/src/components/museum/components/artwork/artworkFilter.module.css
+++ b/src/components/museum/components/artwork/artworkFilter.module.css
@@ -2,39 +2,6 @@
margin-bottom: 24px;
}
-/* 등록 상태 토글 */
-.applicatedToggle {
- margin-bottom: 16px;
- display: flex;
- justify-content: center;
-}
-
-.toggleButton {
- padding: 8px 16px;
- border: 1px solid #f37021;
- background-color: white;
- color: #f37021;
- border-radius: 20px;
- font-family: 'Pretendard', sans-serif;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.toggleButton:hover {
- background-color: #fef7f3;
-}
-
-.toggleButtonActive {
- background-color: #f37021;
- color: white;
-}
-
-.toggleButtonActive:hover {
- background-color: #e05a1a;
-}
-
.searchContainer {
margin-bottom: 16px;
}
diff --git a/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx b/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
index 4185d56..557f7b9 100644
--- a/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
@@ -1,3 +1,4 @@
+import { useNavigate } from 'react-router-dom';
import styles from './exhibitionArtworkModal.module.css';
export default function ExhibitionArtworkModal({
@@ -8,6 +9,8 @@ export default function ExhibitionArtworkModal({
isThumbnail = false,
isChangeMode = false
}) {
+ const navigate = useNavigate();
+
if (!isOpen) return null;
const handleOverlayClick = (e) => {
@@ -17,18 +20,15 @@ export default function ExhibitionArtworkModal({
};
const handleNewArtwork = () => {
- // 숨겨진 파일 입력 요소 클릭
- const fileInput = document.getElementById('artworkFileInput');
- if (fileInput) {
- fileInput.click();
- }
- };
-
- const handleFileChange = (e) => {
- const file = e.target.files[0];
- if (file && onNewArtwork) {
- onNewArtwork(file);
- }
+ // 새 작품 등록 페이지로 이동
+ navigate('/artwork/upload', {
+ state: {
+ fromExhibition: true,
+ currentArtworkIndex: isThumbnail ? -1 : 0,
+ isChangeMode,
+ returnTo: 'exhibition-upload'
+ }
+ });
onClose();
};
@@ -103,15 +103,6 @@ export default function ExhibitionArtworkModal({
-
- {/* 숨겨진 파일 입력 요소 */}
-
);
}
diff --git a/src/components/museum/components/exhibition/ExhibitionCard.jsx b/src/components/museum/components/exhibition/ExhibitionCard.jsx
index 48ee8f1..31a967a 100644
--- a/src/components/museum/components/exhibition/ExhibitionCard.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionCard.jsx
@@ -47,12 +47,12 @@ export default function ExhibitionCard({
- {showStatus && exhibition.status && (
-
- {exhibition.status}
+ {showStatus && exhibition.status === 'ONGOING' && (
+
+ 전시 중
)}
{isEditMode && (
@@ -74,12 +74,12 @@ export default function ExhibitionCard({
- {showStatus && exhibition.status && (
-
- {exhibition.status}
+ {showStatus && exhibition.status === 'ONGOING' && (
+
+ 전시 중
)}
{isEditMode && (
@@ -90,17 +90,16 @@ export default function ExhibitionCard({
)}
-
- {showDate && exhibition.createdAt && (
-
- {exhibition.createdAt}
-
- )}
{showDescription && (
{exhibition.title}
+ {showDate && exhibition.startDate && exhibition.endDate && (
+
+ {`${exhibition.startDate.slice(2, 4)}.${exhibition.startDate.slice(5, 7)}.${exhibition.startDate.slice(8, 10)} - ${exhibition.endDate.slice(2, 4)}.${exhibition.endDate.slice(5, 7)}.${exhibition.endDate.slice(8, 10)}`}
+
+ )}
{exhibition.description}
)}
diff --git a/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx b/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
index f4d4a7c..2c08b68 100644
--- a/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
@@ -7,12 +7,17 @@ import styles from './exhibitionDatePicker.module.css';
export default function ExhibitionDatePicker() {
const navigate = useNavigate();
const location = useLocation();
+
+ // 업로드 페이지에서 넘어온 draft/initialDates/returnTo
+ const draft = location.state?.draft;
+ const returnTo = location.state?.returnTo || -1;
+
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedStartDate, setSelectedStartDate] = useState(null);
const [selectedEndDate, setSelectedEndDate] = useState(null);
const [isSelectingEndDate, setIsSelectingEndDate] = useState(false);
- // URL 파라미터나 state에서 초기 날짜 가져오기
+ // URL state에서 초기 날짜 가져오기
useEffect(() => {
if (location.state?.initialDates) {
const { startDate, endDate } = location.state.initialDates;
@@ -21,22 +26,15 @@ export default function ExhibitionDatePicker() {
}
}, [location.state]);
- // 현재 월의 첫 번째 날과 마지막 날 계산
+ // 현재 월 계산
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
-
- // 현재 월의 첫 번째 요일 (0: 일요일, 1: 월요일, ...)
const firstDayOfWeek = firstDayOfMonth.getDay();
-
- // 현재 월의 총 일수
const daysInMonth = lastDayOfMonth.getDate();
- // 이전 월로 이동
const goToPreviousMonth = () => {
setCurrentDate(prev => new Date(prev.getFullYear(), prev.getMonth() - 1, 1));
};
-
- // 다음 월로 이동
const goToNextMonth = () => {
setCurrentDate(prev => new Date(prev.getFullYear(), prev.getMonth() + 1, 1));
};
@@ -44,22 +42,15 @@ export default function ExhibitionDatePicker() {
// 날짜 선택 처리
const handleDateClick = (day) => {
const clickedDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
-
- // 등록일 기준 7일 뒤부터 선택 가능
const today = new Date();
- const minDate = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
-
- if (clickedDate < minDate) {
- return; // 선택 불가능한 날짜
- }
+ const minDate = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000); // 7일 뒤부터
+ if (clickedDate < minDate) return;
if (!selectedStartDate || (selectedStartDate && selectedEndDate)) {
- // 시작일 선택 또는 재선택
setSelectedStartDate(clickedDate);
setSelectedEndDate(null);
setIsSelectingEndDate(true);
} else if (selectedStartDate && !selectedEndDate) {
- // 종료일 선택
if (clickedDate >= selectedStartDate) {
setSelectedEndDate(clickedDate);
setIsSelectingEndDate(false);
@@ -67,7 +58,6 @@ export default function ExhibitionDatePicker() {
}
};
- // 날짜가 선택 가능한지 확인
const isDateSelectable = (day) => {
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
const today = new Date();
@@ -75,38 +65,38 @@ export default function ExhibitionDatePicker() {
return date >= minDate;
};
- // 날짜가 선택된 시작일인지 확인
const isStartDate = (day) => {
if (!selectedStartDate) return false;
- return day === selectedStartDate.getDate() &&
- currentDate.getMonth() === selectedStartDate.getMonth() &&
- currentDate.getFullYear() === selectedStartDate.getFullYear();
+ return (
+ day === selectedStartDate.getDate() &&
+ currentDate.getMonth() === selectedStartDate.getMonth() &&
+ currentDate.getFullYear() === selectedStartDate.getFullYear()
+ );
};
- // 날짜가 선택된 종료일인지 확인
const isEndDate = (day) => {
if (!selectedEndDate) return false;
- return day === selectedEndDate.getDate() &&
- currentDate.getMonth() === selectedEndDate.getMonth() &&
- currentDate.getFullYear() === selectedEndDate.getFullYear();
+ return (
+ day === selectedEndDate.getDate() &&
+ currentDate.getMonth() === selectedEndDate.getMonth() &&
+ currentDate.getFullYear() === selectedEndDate.getFullYear()
+ );
};
- // 날짜가 선택 범위 내에 있는지 확인
const isInRange = (day) => {
if (!selectedStartDate || !selectedEndDate) return false;
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
return date > selectedStartDate && date < selectedEndDate;
};
- // 선택된 기간의 총 일수 계산
const getTotalDays = () => {
if (!selectedStartDate || !selectedEndDate) return 0;
const diffTime = selectedEndDate.getTime() - selectedStartDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays + 1; // 시작일과 종료일 포함
+ // (선택된 시작/종료일이 동일하면 1일)
};
- // 날짜 포맷팅
const formatDate = (date) => {
if (!date) return '';
return (
@@ -127,28 +117,23 @@ export default function ExhibitionDatePicker() {
);
};
- // 캘린더 그리드 생성
const generateCalendarDays = () => {
const days = [];
-
- // 이전 월의 마지막 날들 (빈 칸 채우기)
for (let i = 0; i < firstDayOfWeek; i++) {
days.push(
);
}
-
- // 현재 월의 날짜들
for (let day = 1; day <= daysInMonth; day++) {
const isSelectable = isDateSelectable(day);
const isStart = isStartDate(day);
const isEnd = isEndDate(day);
const inRange = isInRange(day);
-
+
let dayClass = styles.day;
if (!isSelectable) dayClass += ` ${styles.disabledDay}`;
if (isStart) dayClass += ` ${styles.startDate}`;
if (isEnd) dayClass += ` ${styles.endDate}`;
if (inRange) dayClass += ` ${styles.inRange}`;
-
+
days.push(
);
}
-
return days;
};
- // 뒤로가기 처리
const handleBack = () => {
- navigate(-1);
+ // 뒤로 가되, draft를 유지하고 싶으면 state로 넘겨줌
+ navigate(-1, {
+ state: {
+ draft
+ }
+ });
};
- // 완료 버튼 클릭 처리
const handleComplete = () => {
if (selectedStartDate && selectedEndDate) {
- // 전시 등록 페이지로 돌아가면서 선택된 날짜 전달
- navigate(-1, {
- state: {
- selectedDates: {
- startDate: selectedStartDate,
- endDate: selectedEndDate,
- totalDays: getTotalDays()
+ // 로컬 시간 기준으로 YYYY-MM-DD 형식 변환 (UTC 시간대 문제 해결)
+ const formatDate = (date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ };
+
+ const startDateString = formatDate(selectedStartDate);
+ const endDateString = formatDate(selectedEndDate);
+
+ // draft에 날짜 데이터를 YYYY-MM-DD 형식으로 추가
+ const updatedDraft = {
+ ...draft,
+ startDate: startDateString,
+ endDate: endDateString,
+ totalDays: getTotalDays()
+ };
+
+ console.log('ExhibitionDatePicker - 날짜 데이터가 포함된 draft:', updatedDraft);
+ console.log('변환된 날짜:', { startDate: startDateString, endDate: endDateString });
+
+ // 업로드 페이지로 돌아가면서 선택된 날짜 + draft 같이 전달
+ // 업로드 페이지에서 location.state.selectedDates & .draft를 사용해 복원
+ if (returnTo === 'exhibition-upload') {
+ navigate('/exhibition/upload', {
+ state: {
+ selectedDates: {
+ startDate: startDateString,
+ endDate: endDateString,
+ totalDays: getTotalDays()
+ },
+ draft: updatedDraft
+ }
+ });
+ } else {
+ // returnTo 정보가 없을 때는 업로드 경로를 직접 지정해도 됨
+ navigate('/exhibition/upload', {
+ replace: true,
+ state: {
+ selectedDates: {
+ startDate: startDateString,
+ endDate: endDateString,
+ totalDays: getTotalDays()
+ },
+ draft: updatedDraft
}
- }
- });
+ });
+ }
}
};
- // 완료 버튼 활성화 여부
const isCompleteButtonActive = selectedStartDate && selectedEndDate;
return (
@@ -244,9 +269,9 @@ export default function ExhibitionDatePicker() {
{/* 안내 메시지 */}
-
+
전시 시작일은 등록일 기준 7일 뒤부터 설정 가능합니다
-
+
{/* 선택된 기간 정보 */}
diff --git a/src/components/museum/components/exhibition/ExhibitionList.jsx b/src/components/museum/components/exhibition/ExhibitionList.jsx
index 5271389..da5f565 100644
--- a/src/components/museum/components/exhibition/ExhibitionList.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionList.jsx
@@ -6,6 +6,7 @@ import chevronLeft from '@/assets/museum/chevron-left.png';
import arrowDown from '@/assets/museum/arrow-down.svg';
import xImage from '@/assets/museum/x.png';
import styles from './exhibitionList.module.css';
+import { getMyExhibitions } from '@/apis/museum/exhibition';
export default function ExhibitionList({
showAddButton = true,
@@ -14,7 +15,7 @@ export default function ExhibitionList({
onBack,
onExhibitionClick
}) {
- // 임시 전시 데이터 (추후 store로 대체)
+ // 전시 데이터 상태
const [exhibitions, setExhibitions] = useState([]);
const [layoutMode, setLayoutMode] = useState('grid');
const [searchKeyword, setSearchKeyword] = useState('');
@@ -23,6 +24,22 @@ export default function ExhibitionList({
const [selectedExhibitions, setSelectedExhibitions] = useState(new Set());
const headerRef = useRef();
+ // 전시 데이터 로드
+ useEffect(() => {
+ const loadExhibitions = async () => {
+ try {
+ const response = await getMyExhibitions({ pageNum: 1, pageSize: 20, fillAll: true });
+ if (response?.data?.data) {
+ setExhibitions(response.data.data.content || []);
+ }
+ } catch (error) {
+ console.error('전시 데이터 로드 실패:', error);
+ }
+ };
+
+ loadExhibitions();
+ }, []);
+
// 필터된 전시 목록
const filteredExhibitions = exhibitions.filter(exhibition =>
exhibition.title.toLowerCase().includes(searchKeyword.toLowerCase()) ||
@@ -194,9 +211,9 @@ export default function ExhibitionList({
) : (
// 정상적인 전시 목록
- {filteredExhibitions.map((exhibition, index) => (
-
-
(
+
+
maxVisible;
+ const hasMoreArtworks = totalElements > maxVisible;
const handleShowMore = () => {
navigate('/artwork/my'); // 내 작품 페이지로 이동
@@ -21,27 +22,38 @@ export default function ArtworkSection({ artworks = [] }) {
내 작품
- _ {artworks.length}개
+ _ {totalElements}개
- {visibleArtworks.map((artwork, index) => (
-
- ))}
+ {visibleArtworks.map((artwork, index) => {
+ const imageUrl = getImageUrl(artwork);
+ return (
+
+ );
+ })}
{hasMoreArtworks && (
{
+ const imageUrl = getImageUrl(artworks[maxVisible]);
+ return imageUrl ? `url(${imageUrl})` : 'none';
+ })(),
+ backgroundColor: (() => {
+ const imageUrl = getImageUrl(artworks[maxVisible]);
+ return imageUrl ? 'transparent' : '#f0f0f0';
+ })()
}}
onClick={handleShowMore}
>
diff --git a/src/components/museum/components/museum/ExhibitionSection.jsx b/src/components/museum/components/museum/ExhibitionSection.jsx
index d1175aa..525d953 100644
--- a/src/components/museum/components/museum/ExhibitionSection.jsx
+++ b/src/components/museum/components/museum/ExhibitionSection.jsx
@@ -1,10 +1,22 @@
import exhibitionStyles from './exhibitionSection.module.css';
import commonStyles from './common.module.css';
-export default function ExhibitionSection({ exhibitions = [] }) {
+export default function ExhibitionSection({ exhibitions = [], totalElements = 0 }) {
+ console.log('ExhibitionSection 렌더링:', { exhibitions, totalElements, exhibitionsType: typeof exhibitions, totalElementsType: typeof totalElements });
+
const maxVisible = 5;
const visibleExhibitions = exhibitions.slice(0, maxVisible);
- const hasMoreExhibitions = exhibitions.length > maxVisible;
+ const hasMoreExhibitions = totalElements > maxVisible;
+
+ // 날짜 형식 변환 헬퍼 함수
+ const formatDate = (dateString) => {
+ if (!dateString) return '';
+ const date = new Date(dateString);
+ const year = date.getFullYear().toString().slice(2); // 24, 25
+ const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 12, 02
+ const day = date.getDate().toString().padStart(2, '0'); // 05, 19
+ return `${year}.${month}.${day}`;
+ };
const handleShowMore = () => {
console.log('전체 전시 페이지로 이동');
@@ -15,23 +27,31 @@ export default function ExhibitionSection({ exhibitions = [] }) {
내 전시
- _ {exhibitions.length}개
+ _ {totalElements}개
{visibleExhibitions.map((exhibition, index) => (
-
+
-
{exhibition.title}
-
{exhibition.date}
-
+
+ {exhibition.title || `전시 ${exhibition.exhibitionId || index + 1}`}
+
+
+ {exhibition.startDate && exhibition.endDate ?
+ `${formatDate(exhibition.startDate)} - ${formatDate(exhibition.endDate)}` :
+ '등록 완료'
+ }
+
+
))}
@@ -40,7 +60,8 @@ export default function ExhibitionSection({ exhibitions = [] }) {
@@ -51,7 +72,7 @@ export default function ExhibitionSection({ exhibitions = [] }) {
)}
diff --git a/src/components/museum/components/museum/MuseumProfile.jsx b/src/components/museum/components/museum/MuseumProfile.jsx
index 94e71cc..867ce5f 100644
--- a/src/components/museum/components/museum/MuseumProfile.jsx
+++ b/src/components/museum/components/museum/MuseumProfile.jsx
@@ -7,12 +7,12 @@ export default function MuseumProfile({ user }) {
-
{user.name}
-
{user.title}
+
{user.nickname || '사용자'}
+
크리에이터의 전시장
diff --git a/src/components/museum/components/museum/museumProfile.module.css b/src/components/museum/components/museum/museumProfile.module.css
index 522611b..fba0400 100644
--- a/src/components/museum/components/museum/museumProfile.module.css
+++ b/src/components/museum/components/museum/museumProfile.module.css
@@ -41,4 +41,5 @@
letter-spacing: -0.35px;
line-height: 25px;
margin: 0;
+ margin-top: 2px;
}
diff --git a/src/components/museum/pages/ArtworkLibraryPage.jsx b/src/components/museum/pages/ArtworkLibraryPage.jsx
index f270bea..004a707 100644
--- a/src/components/museum/pages/ArtworkLibraryPage.jsx
+++ b/src/components/museum/pages/ArtworkLibraryPage.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import ArtworkCard from '@museum/components/artwork/ArtworkCard';
-import useArtworkStore from '@museum/services/artworkStore';
+import { useInfinitePieces } from '@apis/museum/artwork';
import chevronLeft from '@/assets/museum/chevron-left.png';
import styles from './artworkLibraryPage.module.css';
@@ -12,14 +12,14 @@ export default function ArtworkLibraryPage() {
// 전시 등록 페이지에서 전달받은 정보
const { fromExhibition, artworkIndex, isThumbnail, isChangeMode } = location.state || {};
- // Zustand 스토어에서 상태 가져오기
+ // API를 사용한 작품 목록 관리
const {
- artworks,
- getFilteredArtworks,
- loadArtworks,
- loadMoreArtworks,
- hasMore
- } = useArtworkStore();
+ pieces: artworks,
+ loading,
+ hasMore,
+ loadMorePieces,
+ resetPieces
+ } = useInfinitePieces({ applicated: true, pageSize: 6 });
// 선택된 작품들 관리
const [selectedArtworks, setSelectedArtworks] = useState(new Set());
@@ -27,29 +27,29 @@ export default function ArtworkLibraryPage() {
// 무한 스크롤을 위한 ref들
const observerRef = useRef();
- const loadingRef = useRef();
+ const lastArtworkRef = useRef();
// 필터된 작품 목록 (검색어 없이 모든 작품)
- const filteredArtworks = getFilteredArtworks();
+ const filteredArtworks = artworks;
- // 컴포넌트 마운트 시 작품 데이터 로드 (라이브러리 페이지에서는 6개)
+ // 컴포넌트 마운트 시 작품 데이터 초기화
useEffect(() => {
- loadArtworks(true, 6);
- }, [loadArtworks]);
+ resetPieces();
+ }, [resetPieces]);
// 무한 스크롤 구현 (IntersectionObserver 사용)
const lastArtworkElementRef = useCallback((node) => {
- if (artworks.isLoadingMore) return;
+ if (loading || !hasMore) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(entries => {
- if (entries[0].isIntersecting && hasMore()) {
- loadMoreArtworks();
+ if (entries[0].isIntersecting && hasMore) {
+ loadMorePieces();
}
});
if (node) observerRef.current.observe(node);
- }, [artworks.isLoadingMore, hasMore, loadMoreArtworks]);
+ }, [loading, hasMore, loadMorePieces]);
const handleBack = () => {
if (fromExhibition) {
@@ -67,16 +67,25 @@ export default function ArtworkLibraryPage() {
};
const handleArtworkSelection = (artwork) => {
+ const artworkId = artwork.pieceId || artwork.id; // pieceId 우선, 없으면 id 사용
+
+ console.log('작품 선택:', {
+ artwork,
+ artworkId,
+ currentSelected: Array.from(selectedArtworks),
+ isChangeMode
+ });
+
if (isChangeMode) {
// 변경 모드: 한 장만 선택 가능
- setSelectedArtworks(new Set([artwork.id]));
+ setSelectedArtworks(new Set([artworkId]));
} else {
// 새로 추가 모드: 여러 장 선택 가능
const newSelected = new Set(selectedArtworks);
- if (newSelected.has(artwork.id)) {
- newSelected.delete(artwork.id);
+ if (newSelected.has(artworkId)) {
+ newSelected.delete(artworkId);
} else {
- newSelected.add(artwork.id);
+ newSelected.add(artworkId);
}
setSelectedArtworks(newSelected);
}
@@ -87,7 +96,7 @@ export default function ArtworkLibraryPage() {
// 선택된 작품들 가져오기
const selectedArtworkList = filteredArtworks.filter(artwork =>
- selectedArtworks.has(artwork.id)
+ selectedArtworks.has(artwork.pieceId || artwork.id)
);
if (fromExhibition && selectedArtworkList.length > 0) {
@@ -103,6 +112,18 @@ export default function ArtworkLibraryPage() {
}
};
+ const handleArtworkClick = (artwork) => {
+ // 작품 클릭 시에는 아무 동작하지 않음 (체크박스만 동작)
+ // 필요시 작품 상세 페이지로 이동하는 로직 추가 가능
+ console.log('작품 클릭:', artwork);
+ };
+
+ // 작품 삭제 완료 시 작품 목록 새로고침
+ const handleArtworkDeleted = (deletedIds) => {
+ console.log('삭제된 작품 ID들:', deletedIds);
+ // 작품 목록을 새로고침
+ resetPieces();
+ };
return (
@@ -128,26 +149,27 @@ export default function ArtworkLibraryPage() {
// 마지막 요소에 ref 추가 (무한 스크롤용)
const isLast = index === filteredArtworks.length - 1;
return (
-
-
handleArtworkSelection(artwork)}
- showDate={true}
- showStatus={false}
- showDescription={true}
- isEditMode={true}
- isSelected={selectedArtworks.has(artwork.id)}
- onSelect={handleArtworkSelection}
- isLibraryMode={true}
- checkboxSize="large"
- />
+
);
})}
{/* 더 로딩 중 표시 */}
- {artworks.isLoadingMore && (
+ {loading && (
더 많은 작품을 불러오는 중...
diff --git a/src/components/museum/pages/ArtworkUploadPage.jsx b/src/components/museum/pages/ArtworkUploadPage.jsx
index 524b23e..e0b850e 100644
--- a/src/components/museum/pages/ArtworkUploadPage.jsx
+++ b/src/components/museum/pages/ArtworkUploadPage.jsx
@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import useArtworkDraftStore from '@museum/services/artworkDraftStore';
-import useArtworkStore from '@museum/services/artworkStore';
-import useUserStore from '@/stores/userStore';
+import { createPiece } from '@apis/museum/artwork';
+import { getCurrentUser } from '@apis/user/user';
import ArtworkModal from '@museum/components/artwork/ArtworkModal';
import chevronLeft from '@/assets/museum/chevron-left.png';
import cameraIcon from '@/assets/user/camera.png';
@@ -11,10 +10,7 @@ import styles from './artworkUploadPage.module.css';
export default function ArtworkUploadPage() {
const navigate = useNavigate();
- const { saveDraft } = useArtworkDraftStore();
- const { addArtwork } = useArtworkStore();
- const { user } = useUserStore();
-
+ const [user, setUser] = useState(null);
const [formData, setFormData] = useState({
title: '',
description: '',
@@ -29,6 +25,132 @@ export default function ArtworkUploadPage() {
const [errorMessage, setErrorMessage] = useState('');
const [showErrors, setShowErrors] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [validationTimeout, setValidationTimeout] = useState(null);
+ const [mainImageUrl, setMainImageUrl] = useState('');
+ const [detailImageUrls, setDetailImageUrls] = useState([]);
+
+ // 유저 정보 가져오기
+ useEffect(() => {
+ const fetchUser = async () => {
+ try {
+ const response = await getCurrentUser();
+ setUser(response.data);
+ } catch (error) {
+ console.error('유저 정보 조회 실패:', error);
+ // 에러가 발생해도 페이지는 계속 사용할 수 있도록 함
+ }
+ };
+
+ fetchUser();
+ }, []);
+
+ // 메인 이미지 URL 생성 및 정리
+ useEffect(() => {
+ if (formData.mainImage) {
+ const url = URL.createObjectURL(formData.mainImage);
+ setMainImageUrl(url);
+
+ // 컴포넌트 언마운트 시 URL 정리
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setMainImageUrl('');
+ }
+ }, [formData.mainImage]);
+
+ // 디테일 이미지 URL 생성 및 정리
+ useEffect(() => {
+ const urls = [];
+ formData.detailImages.forEach((image, index) => {
+ if (image) {
+ const url = URL.createObjectURL(image);
+ urls[index] = url;
+ }
+ });
+
+ setDetailImageUrls(urls);
+
+ // 컴포넌트 언마운트 시 모든 URL 정리
+ return () => {
+ urls.forEach(url => {
+ if (url) URL.revokeObjectURL(url);
+ });
+ };
+ }, [formData.detailImages]);
+
+ // 입력 완료 후 1초 뒤에 에러 메시지만 숨기기
+ useEffect(() => {
+ if (validationTimeout) {
+ clearTimeout(validationTimeout);
+ }
+
+ const timeout = setTimeout(() => {
+ // 입력이 완료되면 에러 메시지 숨기기
+ if (showErrors) {
+ setShowErrors(false);
+ setErrorMessage('');
+ }
+ }, 1000);
+
+ setValidationTimeout(timeout);
+
+ return () => {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ };
+ }, [formData.title, formData.description, showErrors]);
+
+ // 폼 검증 함수
+ const validateForm = () => {
+ let hasError = false;
+ let errorMsg = '';
+
+ // 1순위: 메인 이미지 없음
+ if (!formData.mainImage) {
+ errorMsg = '메인 이미지를 등록해주세요';
+ hasError = true;
+ }
+ // 2순위: 작품 소개 500자 이상
+ else if (formData.description && formData.description.length > 500) {
+ errorMsg = '작품 소개는 최대 500자까지만 가능해요';
+ hasError = true;
+ }
+ // 3순위: 작품명 없음
+ else if (!formData.title || !formData.title.trim()) {
+ errorMsg = '작품명을 작성해주세요';
+ hasError = true;
+ }
+ // 4순위: 작품 소개 없음
+ else if (!formData.description || !formData.description.trim()) {
+ errorMsg = '작품 소개를 작성해주세요';
+ hasError = true;
+ }
+
+ // 에러가 있을 때만 상태 업데이트
+ if (hasError) {
+ setErrorMessage(errorMsg);
+ setShowErrors(true);
+
+ // 에러 메시지를 3초 후에 자동으로 숨기기
+ setTimeout(() => {
+ setShowErrors(false);
+ setErrorMessage('');
+ }, 3000);
+ }
+
+ return hasError; // 에러 여부 반환
+ };
+
+ // 로컬 draft 저장소 (localStorage 사용)
+ const saveDraft = (data) => {
+ try {
+ localStorage.setItem('artworkDraft', JSON.stringify(data));
+ console.log('임시저장 완료:', data);
+ } catch (error) {
+ console.error('임시저장 실패:', error);
+ }
+ };
const handleBack = () => {
// 작성 중인 내용이 있으면 취소 모달 표시
@@ -85,38 +207,11 @@ export default function ArtworkUploadPage() {
const handleSubmit = (e) => {
e.preventDefault();
- let hasError = false;
-
- // 1순위: 메인 이미지 없음
- if (!formData.mainImage) {
- setErrorMessage('메인 이미지를 등록해주세요');
- hasError = true;
- }
- // 2순위: 작품 소개 500자 이상
- else if (formData.description && formData.description.length > 500) {
- setErrorMessage('작품 소개는 최대 500자까지만 가능해요');
- hasError = true;
- }
- // 3순위: 작품명 없음
- else if (!formData.title || !formData.title.trim()) {
- setErrorMessage('작품명을 작성해주세요');
- hasError = true;
- }
- // 4순위: 작품 소개 없음
- else if (!formData.description || !formData.description.trim()) {
- setErrorMessage('작품 소개를 작성해주세요');
- hasError = true;
- }
-
- setShowErrors(hasError);
+ // 즉시 검증 실행하고 에러 여부 확인
+ const hasError = validateForm();
- // 에러가 있으면 에러 메시지 표시
+ // 에러가 있으면 제출 중단
if (hasError) {
- // 에러 메시지를 3초 후에 자동으로 숨기기
- setTimeout(() => {
- setShowErrors(false);
- setErrorMessage('');
- }, 3000);
return;
}
@@ -124,26 +219,36 @@ export default function ArtworkUploadPage() {
setModal({ isOpen: true, type: 'register' });
};
- const handleRegisterConfirm = () => {
- // 실제 작품 등록 로직
- const newArtwork = {
- title: formData.title,
- description: formData.description,
- image: formData.mainImage ? URL.createObjectURL(formData.mainImage) : null,
- // 추가 정보들
- artistName: user.name,
- artistNickname: user.nickname,
- // 이미지 파일들 (실제 구현에서는 서버에 업로드 후 URL 사용)
- mainImage: formData.mainImage,
- detailImages: formData.detailImages.filter(img => img !== null)
- };
+ const handleRegisterConfirm = async () => {
+ if (isSubmitting) return;
- // store에 작품 등록
- const registeredArtwork = addArtwork(newArtwork);
- console.log('작품 등록 완료:', registeredArtwork);
+ setIsSubmitting(true);
- // 등록 완료 모달 표시
- setModal({ isOpen: true, type: 'complete' });
+ try {
+ // API를 사용하여 작품 등록
+ const response = await createPiece(formData, 'APPLICATION');
+
+ if (response?.success === true && (response?.code === 200 || response?.code === 201)) {
+ console.log('작품 등록 완료:', response.data);
+
+ // 등록 완료 모달 표시
+ setModal({ isOpen: true, type: 'complete' });
+ } else {
+ throw new Error('작품 등록에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('작품 등록 실패:', error);
+ setErrorMessage('작품 등록에 실패했습니다. 다시 시도해주세요.');
+ setShowErrors(true);
+
+ // 에러 메시지를 3초 후에 자동으로 숨기기
+ setTimeout(() => {
+ setShowErrors(false);
+ setErrorMessage('');
+ }, 3000);
+ } finally {
+ setIsSubmitting(false);
+ }
};
const handleCompleteConfirm = () => {
@@ -169,19 +274,47 @@ export default function ArtworkUploadPage() {
setModal({ isOpen: false, type: null });
};
- const handleSave = () => {
- // 수동 임시 저장
- saveDraft(formData);
+ const handleSave = async () => {
+ // 임시저장 중이면 중단
+ if (isSubmitting) return;
- // 임시저장 완료 메시지 표시
- setErrorMessage('임시 저장이 완료되었어요');
- setShowErrors(true);
+ setIsSubmitting(true);
- // 메시지를 3초 후에 자동으로 숨기기
- setTimeout(() => {
- setShowErrors(false);
- setErrorMessage('');
- }, 3000);
+ try {
+ // API를 사용하여 임시저장 (DRAFT)
+ const response = await createPiece(formData, 'DRAFT');
+
+ if (response?.success === true && (response?.code === 200 || response?.code === 201)) {
+ console.log('임시저장 완료:', response.data);
+
+ // 로컬 draft도 업데이트
+ saveDraft(formData);
+
+ // 임시저장 완료 메시지 표시
+ setErrorMessage('임시 저장이 완료되었어요');
+ setShowErrors(true);
+
+ // 메시지를 3초 후에 자동으로 숨기기
+ setTimeout(() => {
+ setShowErrors(false);
+ setErrorMessage('');
+ }, 3000);
+ } else {
+ throw new Error('임시저장에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('임시저장 실패:', error);
+ setErrorMessage('임시저장에 실패했습니다. 다시 시도해주세요.');
+ setShowErrors(true);
+
+ // 에러 메시지를 3초 후에 자동으로 숨기기
+ setTimeout(() => {
+ setShowErrors(false);
+ setErrorMessage('');
+ }, 3000);
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
@@ -214,23 +347,23 @@ export default function ArtworkUploadPage() {
메인 이미지
-
- {formData.mainImage ? (
-
- ) : (
-
-
-
- 작품 사진을 등록해주세요
- (필수)
-
-
- )}
-
+
+ {mainImageUrl ? (
+
+ ) : (
+
+
+
+ 작품 사진을 등록해주세요
+ (필수)
+
+
+ )}
+
디테일 컷{index + 1}
-
- {formData.detailImages[index] ? (
-
-
-
{
- e.preventDefault();
- removeDetailImage(index);
- }}
- className={styles.removeImageButton}
- >
- ×
-
-
- ) : (
-
-
-
- 디테일 컷을 추가해보세요
- (최대 5장)
-
-
- )}
-
+
+ {detailImageUrls[index] ? (
+
+
+
{
+ e.preventDefault();
+ removeDetailImage(index);
+ }}
+ className={styles.removeImageButton}
+ >
+ ×
+
+
+ ) : (
+
+
+
+ 디테일 컷을 추가해보세요
+ (최대 5장)
+
+
+ )}
+
-
- VR 등록하기
-
연락 정보 등록하기
diff --git a/src/components/museum/pages/DraftArtworkPage.jsx b/src/components/museum/pages/DraftArtworkPage.jsx
index 225557e..2cf2681 100644
--- a/src/components/museum/pages/DraftArtworkPage.jsx
+++ b/src/components/museum/pages/DraftArtworkPage.jsx
@@ -1,36 +1,43 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import DraftArtworkList from '@museum/components/artwork/DraftArtworkList';
-import styles from './draftArtworkPage.module.css';
+import ArtworkList from '@museum/components/artwork/ArtworkList';
+import { useInfinitePieces } from '@apis/museum/artwork';
+import styles from './myArtworkPage.module.css'; // 같은 스타일 사용
export default function DraftArtworkPage() {
const navigate = useNavigate();
- // 스크롤 상태 관리
- const [isScrolled, setIsScrolled] = useState(false);
-
- // 스크롤 이벤트 처리
- useEffect(() => {
- const handleScroll = () => {
- const scrollTop = window.scrollY;
- setIsScrolled(scrollTop > 10);
- };
-
- window.addEventListener('scroll', handleScroll);
- return () => window.removeEventListener('scroll', handleScroll);
- }, []);
+ // API를 사용한 임시저장 작품 목록 관리 (applicated: false)
+ const {
+ pieces: draftArtworks,
+ loading,
+ hasMore,
+ loadMorePieces: loadMoreDrafts,
+ resetPieces
+ } = useInfinitePieces({ applicated: false, pageSize: 3 });
const handleBack = () => {
- navigate('/artwork'); // 내 작품 페이지로 돌아가기
+ navigate('/artwork/my'); // 내 작품 페이지로 돌아가기
};
const handleAddArtwork = () => {
navigate('/artwork/upload'); // 작품 등록 페이지로 이동
};
- const handleArtworkClick = (draft) => {
- // 임시저장 작품 편집 페이지로 이동 (추후 구현)
- console.log('임시저장 작품 클릭:', draft);
+ const handleDraftClick = () => {
+ // 이미 임시저장 페이지에 있으므로 아무것도 하지 않음
+ };
+
+ const handleArtworkClick = (artwork) => {
+ // 작품 상세 페이지로 이동 (추후 구현)
+ console.log('임시저장 작품 클릭:', artwork);
+ };
+
+ // 작품 삭제 완료 시 작품 목록 새로고침
+ const handleArtworkDeleted = (deletedIds) => {
+ console.log('삭제된 임시저장 작품 ID들:', deletedIds);
+ // 작품 목록을 새로고침
+ resetPieces();
};
return (
@@ -38,12 +45,20 @@ export default function DraftArtworkPage() {
{/* Status Bar 공간 */}
-
);
diff --git a/src/components/museum/pages/ExhibitionInvitationPage.jsx b/src/components/museum/pages/ExhibitionInvitationPage.jsx
index a462333..13c5dd4 100644
--- a/src/components/museum/pages/ExhibitionInvitationPage.jsx
+++ b/src/components/museum/pages/ExhibitionInvitationPage.jsx
@@ -3,12 +3,9 @@ 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([]);
diff --git a/src/components/museum/pages/ExhibitionParticipantPage.jsx b/src/components/museum/pages/ExhibitionParticipantPage.jsx
index 13fc8fa..7738182 100644
--- a/src/components/museum/pages/ExhibitionParticipantPage.jsx
+++ b/src/components/museum/pages/ExhibitionParticipantPage.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import chevronLeft from '@/assets/museum/chevron-left.png';
import searchIcon from '@/assets/footer/search.svg';
+import { getUserProfilesByCode } from '@/apis/user/user.js';
import styles from './exhibitionParticipantPage.module.css';
export default function ExhibitionParticipantPage() {
@@ -18,26 +19,48 @@ export default function ExhibitionParticipantPage() {
// URL state에서 전시 정보 받아오기
const 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 searchUsers = async (query) => {
+ if (!query.trim()) {
+ setSearchResults([]);
+ setIsSearching(false);
+ return;
+ }
- useEffect(() => {
- // 검색어가 있을 때만 검색 결과 표시
- if (searchQuery.trim()) {
- const filtered = dummyUsers.filter(user =>
- user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
- user.displayName.toLowerCase().includes(searchQuery.toLowerCase())
- );
- setSearchResults(filtered);
- setIsSearching(true);
- } else {
+ setIsSearching(true);
+ try {
+ // @를 앞에 붙여서 API 요청
+ const userCode = `@${query.trim()}`;
+ console.log('사용자 검색 요청:', userCode);
+
+ const response = await getUserProfilesByCode(userCode);
+ console.log('사용자 검색 응답:', response);
+
+ if (response && response.data && Array.isArray(response.data)) {
+ setSearchResults(response.data);
+ } else {
+ setSearchResults([]);
+ }
+ } catch (error) {
+ console.error('사용자 검색 실패:', error);
setSearchResults([]);
+ } finally {
setIsSearching(false);
}
+ };
+
+ // 검색어 변경 시 디바운스 처리
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (searchQuery.trim()) {
+ searchUsers(searchQuery);
+ } else {
+ setSearchResults([]);
+ setIsSearching(false);
+ }
+ }, 500); // 500ms 디바운스
+
+ return () => clearTimeout(timer);
}, [searchQuery]);
const handleBack = () => {
@@ -49,11 +72,12 @@ export default function ExhibitionParticipantPage() {
};
const handleUserSelect = (user) => {
- const isSelected = selectedParticipants.some(p => p.id === user.id);
+ const userId = user.userId || user.id;
+ const isSelected = selectedParticipants.some(p => (p.userId || p.id) === userId);
if (isSelected) {
// 이미 선택된 사용자라면 제거
- setSelectedParticipants(prev => prev.filter(p => p.id !== user.id));
+ setSelectedParticipants(prev => prev.filter(p => (p.userId || p.id) !== userId));
} else {
// 새로운 사용자라면 추가
setSelectedParticipants(prev => [...prev, user]);
@@ -77,7 +101,7 @@ export default function ExhibitionParticipantPage() {
};
const isUserSelected = (userId) => {
- return selectedParticipants.some(p => p.id === userId);
+ return selectedParticipants.some(p => (p.userId || p.id) === userId);
};
return (
@@ -125,21 +149,21 @@ export default function ExhibitionParticipantPage() {
{searchResults.map((user) => (
handleUserSelect(user)}
>
- {user.profileImage ? (
-
+ {user.profileImageUrl ? (
+
) : (
)}
- {user.displayName}
- @{user.username}
+ {user.nickname || user.displayName}
+ @{user.code || user.username}
@@ -153,21 +177,21 @@ export default function ExhibitionParticipantPage() {
선택된 참여자
{selectedParticipants.map((user) => (
handleUserSelect(user)}
>
- {user.profileImage ? (
-
+ {user.profileImageUrl ? (
+
) : (
)}
- {user.displayName}
- @{user.username}
+ {user.nickname || user.displayName}
+ @{user.code || user.username}
diff --git a/src/components/museum/pages/ExhibitionUploadPage.jsx b/src/components/museum/pages/ExhibitionUploadPage.jsx
index 3d1dd18..78aa68b 100644
--- a/src/components/museum/pages/ExhibitionUploadPage.jsx
+++ b/src/components/museum/pages/ExhibitionUploadPage.jsx
@@ -1,27 +1,16 @@
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
-import useUserStore from '@/stores/userStore';
-import useExhibitionPhotoStore from '@museum/services/exhibitionPhotoStore';
import chevronLeft from '@/assets/museum/chevron-left.png';
import cameraIcon from '@/assets/user/camera.png';
import plusCircleIcon from '@/assets/museum/plus-circle.png';
import ExhibitionArtworkModal from '@museum/components/exhibition/ExhibitionArtworkModal';
+import { getUserContact, getCurrentUser } from '@/apis/user/user.js';
+import { createExhibition } from '@/apis/museum/exhibition.js';
import styles from './exhibitionUploadPage.module.css';
export default function ExhibitionUploadPage() {
const navigate = useNavigate();
const location = useLocation();
- const { user, contactInfo } = useUserStore();
-
- // Zustand store 사용
- const {
- thumbnail,
- artworks,
- setThumbnail,
- addArtwork,
- updateArtwork,
- removeArtwork: removeArtworkFromStore
- } = useExhibitionPhotoStore();
// 로컬 상태로 관리 (전시 정보)
const [exhibitionData, setExhibitionData] = useState({
@@ -32,25 +21,226 @@ export default function ExhibitionUploadPage() {
totalDays: 0
});
- // 모달 상태 추가
+
+ // 모달 상태
const [isArtworkModalOpen, setIsArtworkModalOpen] = useState(false);
const [currentArtworkIndex, setCurrentArtworkIndex] = useState(0);
const [isChangeMode, setIsChangeMode] = useState(false);
+
+ // 썸네일 및 작품 상태
+ const [thumbnail, setThumbnail] = useState(null);
+ const [artworks, setArtworks] = useState([]); // 작품 객체 배열 (ID와 imageUrl 포함)
+
+ // 연락 정보 상태
+ const [contactInfo, setContactInfo] = useState({
+ isRegistered: false
+ });
+
+ // 전시 등록 관련 상태
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [offlineLocation, setOfflineLocation] = useState({
+ address: '',
+ addressName: '',
+ offlineDescription: ''
+ });
+ const [participants, setParticipants] = useState([]);
+
+ // 기간 설정 완료 상태 확인 함수
+ const isDateRangeSet = () => {
+ const { startDate, endDate } = exhibitionData;
+
+ console.log('isDateRangeSet 호출됨:', {
+ startDate,
+ endDate,
+ startDateType: typeof startDate,
+ endDateType: typeof endDate,
+ startDateIsDate: startDate instanceof Date,
+ endDateIsDate: endDate instanceof Date
+ });
+
+ // Date 객체인 경우
+ if (startDate instanceof Date && endDate instanceof Date) {
+ const result = !isNaN(startDate.getTime()) && !isNaN(endDate.getTime());
+ console.log('Date 객체 처리 결과:', result);
+ return result;
+ }
+
+ // 문자열인 경우 (YYYY-MM-DD 형식)
+ if (typeof startDate === 'string' && typeof endDate === 'string') {
+ const startPattern = /^\d{4}-\d{2}-\d{2}$/;
+ const endPattern = /^\d{4}-\d{2}-\d{2}$/;
+ const startValid = startPattern.test(startDate);
+ const endValid = endPattern.test(endDate);
+ const result = startValid && endValid;
+
+ console.log('문자열 처리 결과:', {
+ startValid,
+ endValid,
+ startPattern: startPattern.test(startDate),
+ endPattern: endPattern.test(endDate),
+ result
+ });
+
+ return result;
+ }
+
+ console.log('조건에 맞지 않음, false 반환');
+ return false;
+ };
+
+ // 오프라인 장소 등록 완료 상태 확인 함수
+ const isOfflineLocationSet = () => {
+ const { address, addressName, offlineDescription } = offlineLocation;
+
+ console.log('isOfflineLocationSet 호출됨:', {
+ address,
+ addressName,
+ offlineDescription,
+ addressType: typeof address,
+ addressNameType: typeof addressName,
+ offlineDescriptionType: typeof offlineDescription,
+ addressValid: address && address.trim().length > 0,
+ addressNameValid: addressName && addressName.trim().length > 0,
+ offlineDescriptionValid: offlineDescription && offlineDescription.trim().length > 0
+ });
+
+ const result = address && address.trim().length > 0;
+ console.log('오프라인 장소 설정 결과:', result);
+ return result;
+ };
- // URL state에서 선택된 날짜 받아오기
+ // 현재 입력 상태를 하나로 묶어 라우팅 state로 넘길 draft
+ const buildDraft = () => {
+ const draft = {
+ exhibitionData,
+ thumbnail,
+ artworks,
+ offlineLocation,
+ participants,
+ contactRegistered: contactInfo.isRegistered,
+ // API 요청 형식에 맞춰 날짜 데이터 저장 (이미 YYYY-MM-DD 형식)
+ startDate: exhibitionData.startDate || null,
+ endDate: exhibitionData.endDate || null,
+ totalDays: exhibitionData.totalDays
+ };
+
+ console.log('buildDraft 호출됨:', draft);
+ console.log('날짜 형식 확인 - startDate:', draft.startDate, 'endDate:', draft.endDate);
+ return draft;
+ };
+
+ // 기간 설정 페이지에서 돌아왔을 때 데이터 복원 (통합)
useEffect(() => {
- if (location.state?.selectedDates) {
- const { startDate, endDate, totalDays } = location.state.selectedDates;
- setExhibitionData(prev => ({
- ...prev,
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- totalDays
- }));
+ console.log('useEffect 실행 - location.state:', location.state);
+
+ // location.state가 실제로 유효한 데이터를 가지고 있을 때만 처리
+ if (location.state && (location.state.selectedDates || location.state.draft)) {
+ const { selectedDates, draft } = location.state;
+
+ console.log('데이터 복원 시작:', { selectedDates, draft });
+
+ setExhibitionData(prev => {
+ let newData = { ...prev };
+
+ // 1단계: draft 데이터 처리 (기본 데이터 먼저)
+ if (draft) {
+ // draft.exhibitionData에서 날짜 정보는 제외하고 기본 데이터만 설정
+ if (draft.exhibitionData) {
+ // 날짜 관련 필드를 명시적으로 제외
+ const safeExhibitionData = { ...draft.exhibitionData };
+ delete safeExhibitionData.startDate;
+ delete safeExhibitionData.endDate;
+ delete safeExhibitionData.totalDays;
+
+ newData = {
+ ...newData,
+ ...safeExhibitionData
+ };
+ }
+ }
+
+ // 2단계: 기간 설정 데이터 처리 (최우선 - 나중에 덮어씀)
+ if (selectedDates) {
+ const { startDate, endDate, totalDays } = selectedDates;
+
+ newData = {
+ ...newData,
+ startDate: startDate, // 이미 YYYY-MM-DD 문자열
+ endDate: endDate, // 이미 YYYY-MM-DD 문자열
+ totalDays
+ };
+ }
+
+ // 3단계: draft에서 날짜 데이터 복원 (selectedDates가 없을 때만)
+ if (!selectedDates && draft) {
+ // draft에서 직접 날짜 데이터 복원
+ if (draft.startDate && draft.endDate) {
+ console.log('draft에서 날짜 데이터 복원:', {
+ startDate: draft.startDate,
+ endDate: draft.endDate,
+ totalDays: draft.totalDays
+ });
+
+ newData = {
+ ...newData,
+ startDate: draft.startDate,
+ endDate: draft.endDate,
+ totalDays: draft.totalDays || 0
+ };
+ }
+
+ // draft.exhibitionData에서도 날짜 데이터 확인 및 복원
+ if (draft.exhibitionData && draft.exhibitionData.startDate && draft.exhibitionData.endDate) {
+ console.log('draft.exhibitionData에서 날짜 데이터 복원:', {
+ startDate: draft.exhibitionData.startDate,
+ endDate: draft.exhibitionData.endDate,
+ totalDays: draft.exhibitionData.totalDays
+ });
+
+ newData = {
+ ...newData,
+ startDate: draft.exhibitionData.startDate,
+ endDate: draft.exhibitionData.endDate,
+ totalDays: draft.exhibitionData.totalDays || 0
+ };
+ }
+ }
+
+ return newData;
+ });
+
+ // 기타 상태 업데이트 (exhibitionData와 독립적)
+ if (draft) {
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ if (draft.offlineLocation) {
+ console.log('오프라인 장소 데이터 복원:', draft.offlineLocation);
+ setOfflineLocation(draft.offlineLocation);
+ }
+ if (draft.participants) setParticipants(draft.participants);
+ if (typeof draft.contactRegistered === 'boolean') {
+ setContactInfo(prev => ({ ...prev, isRegistered: draft.contactRegistered }));
+ }
+ }
}
}, [location.state]);
- // URL state에서 참여자 정보 받아오기
+ // ✅ URL state에 draft가 있으면 전부 복원 (통합된 useEffect에서 처리하므로 주석 처리)
+ // useEffect(() => {
+ // if (location.state?.draft) {
+ // const d = location.state.draft;
+ // if (d.exhibitionData) setExhibitionData(prev => ({ ...prev, ...d.exhibitionData }));
+ // if (d.thumbnail) setThumbnail(d.thumbnail);
+ // if (d.artworks) setArtworks(d.artworks);
+ // if (d.offlineLocation) setOfflineLocation(d.offlineLocation);
+ // if (d.participants) setParticipants(d.participants);
+ // if (typeof d.contactRegistered === 'boolean') {
+ // setContactInfo(prev => ({ ...prev, isRegistered: d.contactRegistered }));
+ // }
+ // }
+ // }, [location.state]);
+
+ // URL state에서 참여자 정보 받아오기 (필요 시 추가 처리)
useEffect(() => {
if (location.state?.participants) {
console.log('참여자 정보:', location.state.participants);
@@ -58,6 +248,175 @@ export default function ExhibitionUploadPage() {
}
}, [location.state]);
+ // 사용자 연락 정보 확인
+ useEffect(() => {
+ const checkUserContact = async () => {
+ try {
+ const userResponse = await getCurrentUser();
+ if (userResponse && userResponse.data && userResponse.data.userId) {
+ const userId = userResponse.data.userId;
+ const contactResponse = await getUserContact(userId);
+
+ if (contactResponse && contactResponse.data) {
+ const contactData = contactResponse.data;
+ if (contactData.email || contactData.instagram) {
+ setContactInfo(prev => ({
+ ...prev,
+ isRegistered: true
+ }));
+ }
+ }
+ }
+ } catch (error) {
+ console.error('연락 정보 확인 실패:', error);
+ }
+ };
+ checkUserContact();
+ }, []);
+
+ // 연락 정보 등록 페이지에서 돌아왔을 때 전시 정보 복원
+ useEffect(() => {
+ if (location.state?.contactUpdated && location.state?.exhibitionData) {
+ const { exhibitionData: savedExhibitionData, thumbnail: savedThumbnail, artworks: savedArtworks } = location.state;
+
+ console.log('연락 정보 등록 후 복원 데이터:', {
+ savedExhibitionData,
+ savedThumbnail,
+ savedArtworks
+ });
+
+ setExhibitionData(prev => ({
+ ...prev,
+ ...savedExhibitionData
+ }));
+
+ if (savedThumbnail) setThumbnail(savedThumbnail);
+ if (savedArtworks) setArtworks(savedArtworks);
+
+ setContactInfo(prev => ({
+ ...prev,
+ isRegistered: true
+ }));
+ }
+ }, [location.state]);
+
+ // 오프라인 장소 등록 페이지에서 돌아왔을 때 정보 복원
+ useEffect(() => {
+ if (location.state?.offlineLocationData && location.state?.returnTo === 'exhibition-upload') {
+ const { address, addressName, offlineDescription } = location.state.offlineLocationData;
+ setOfflineLocation({
+ address: address || '',
+ addressName: addressName || '',
+ offlineDescription: offlineDescription || ''
+ });
+ }
+ }, [location.state]);
+
+ // 전시 참여자 등록 페이지에서 돌아왔을 때 정보 복원
+ useEffect(() => {
+ if (location.state?.participantsData && location.state?.returnTo === 'exhibition-upload') {
+ setParticipants(location.state.participantsData);
+ }
+ }, [location.state]);
+
+ // 작품 라이브러리에서 돌아왔을 때 선택된 작품들 처리
+ useEffect(() => {
+ if (location.state?.returnFromLibrary && location.state?.selectedArtworks) {
+ const { selectedArtworks, artworkIndex, isThumbnail } = location.state;
+
+ console.log('라이브러리에서 선택된 작품들:', selectedArtworks);
+
+ if (isThumbnail) {
+ // 썸네일로 선택된 경우
+ if (selectedArtworks.length > 0) {
+ setThumbnail(selectedArtworks[0]);
+ }
+ } else {
+ // 전시 작품으로 선택된 경우
+ if (artworkIndex !== undefined) {
+ // 특정 인덱스에 작품 추가/교체
+ if (artworkIndex >= 0) {
+ if (artworkIndex < artworks.length) {
+ // 기존 작품 교체
+ updateArtwork(artworkIndex, selectedArtworks[0]);
+ } else {
+ // 새 작품 추가
+ addArtwork(selectedArtworks[0]);
+ }
+ } else {
+ // 새 작품 추가
+ addArtwork(selectedArtworks[0]);
+ }
+ } else {
+ // 여러 작품 추가
+ selectedArtworks.forEach(artwork => {
+ addArtwork(artwork);
+ });
+ }
+ }
+ }
+ }, [location.state]);
+
+ // 전시 등록 처리
+ const handleSubmitExhibition = async () => {
+ // 필수 입력 검증
+ if (!exhibitionData.title || !exhibitionData.title.trim()) {
+ alert('전시명을 입력해주세요.');
+ return;
+ }
+ if (!exhibitionData.description || !exhibitionData.description.trim()) {
+ alert('전시 소개를 입력해주세요.');
+ return;
+ }
+ if (!exhibitionData.startDate || !exhibitionData.endDate) {
+ alert('전시 기간을 설정해주세요.');
+ return;
+ }
+ if (artworks.length < 3) {
+ alert('작품을 3개 이상 등록해주세요.');
+ return;
+ }
+ if (!contactInfo.isRegistered) {
+ alert('연락 정보를 등록해주세요.');
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ // 실제 pieceIdList 사용
+ const pieceIdList = artworks.map(artwork => artwork.pieceId);
+ const participantIdList = participants.map(p => p.userId);
+
+ // draft에서 날짜 데이터 사용 (이미 YYYY-MM-DD 형식)
+ const startDate = exhibitionData.startDate ? exhibitionData.startDate.toISOString().split('T')[0] : null;
+ const endDate = exhibitionData.endDate ? exhibitionData.endDate.toISOString().split('T')[0] : null;
+
+ const exhibitionPayload = {
+ pieceIdList,
+ endDate,
+ participantIdList,
+ startDate,
+ address: offlineLocation.address || '주소 미입력',
+ title: exhibitionData.title.trim(),
+ offlineDescription: offlineLocation.offlineDescription || '오프라인 전시 설명 미입력',
+ description: exhibitionData.description.trim(),
+ addressName: offlineLocation.addressName || '장소명 미입력'
+ };
+
+ console.log('전시 등록 요청 데이터:', exhibitionPayload);
+ const response = await createExhibition(exhibitionPayload);
+ console.log('전시 등록 성공:', response);
+
+ alert('전시가 성공적으로 등록되었습니다!');
+ navigate('/museum');
+ } catch (error) {
+ console.error('전시 등록 실패:', error);
+ alert('전시 등록에 실패했습니다. 다시 시도해주세요.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
const handleBack = () => {
navigate('/museum');
};
@@ -72,9 +431,7 @@ export default function ExhibitionUploadPage() {
const handleThumbnailChange = (e) => {
const file = e.target.files[0];
- if (file) {
- setThumbnail(file);
- }
+ if (file) setThumbnail(file);
};
// 전시 작품 등록 모달 열기 (새로 추가)
@@ -94,15 +451,11 @@ export default function ExhibitionUploadPage() {
// 새 작품 등록 처리
const handleNewArtwork = (file) => {
if (currentArtworkIndex === -1) {
- // 썸네일인 경우
setThumbnail(file);
} else {
- // 작품인 경우
if (isChangeMode) {
- // 변경 모드: 해당 인덱스의 작품만 수정
updateArtwork(currentArtworkIndex, file);
} else {
- // 새로 추가 모드: 작품 추가
addArtwork(file);
}
}
@@ -110,81 +463,81 @@ export default function ExhibitionUploadPage() {
// 작품 라이브러리에서 가져오기 처리
const handleLoadFromLibrary = () => {
- // 작품 라이브러리 페이지로 이동
navigate('/artwork/library', {
state: {
fromExhibition: true,
currentArtworkIndex,
- isChangeMode
+ isChangeMode,
+ isThumbnail: currentArtworkIndex === -1
}
});
};
- // 작품 제거 처리
+ // 작품 추가/수정/삭제 (객체 기반)
+ const addArtwork = (artwork) => setArtworks(prev => [...prev, artwork]);
+ const updateArtwork = (index, artwork) => {
+ setArtworks(prev => {
+ const next = [...prev];
+ next[index] = artwork;
+ return next;
+ });
+ };
const removeArtwork = (index) => {
- removeArtworkFromStore(index);
+ setArtworks(prev => prev.filter((_, i) => i !== index));
};
- // 기간 설정 페이지로 이동
+ // 기간 설정 페이지로 이동 (초안 동봉!)
const openDatePicker = () => {
navigate('/exhibition/date-picker', {
state: {
initialDates: {
startDate: exhibitionData.startDate,
endDate: exhibitionData.endDate
- }
+ },
+ draft: buildDraft(), // ✅ 현재까지 입력한 값 모두
+ returnTo: 'exhibition-upload'
}
});
};
- // 전시 작품 슬라이드 렌더링
+ // 전시 작품 슬라이드 렌더링 (객체 기반)
const renderArtworkSlides = () => {
const slides = [];
-
- // 기존 작품들 렌더링
artworks.forEach((artwork, index) => {
slides.push(
- {artwork ? (
-
-
-
openChangeArtworkModal(index)}
- >
- 변경
-
-
removeArtwork(index)}
- >
- 삭제
-
+
+
{
+ e.target.style.display = 'none';
+ e.target.nextSibling.style.display = 'block';
+ }}
+ />
+
+ 이미지를 불러올 수 없습니다
- ) : (
-
openArtworkModal(index)}
- style={{ cursor: 'pointer' }}
+
openChangeArtworkModal(index)}
>
-
-
- 전시 작품을 등록해주세요
- (선택)
-
-
- )}
+ 변경
+
+
removeArtwork(index)}
+ >
+ 삭제
+
+
);
});
-
- // 새 작품 추가 버튼 (최대 10개)
+
if (artworks.length < 10) {
slides.push(
@@ -204,7 +557,6 @@ export default function ExhibitionUploadPage() {
);
}
-
return slides;
};
@@ -221,140 +573,160 @@ export default function ExhibitionUploadPage() {
- {/* 전시 썸네일 및 작품 등록 */}
-
-
전시 작품 등록
-
- {/* 전시 썸네일 */}
-
-
- {thumbnail ? (
-
-
-
openChangeArtworkModal(-1)}
- >
- 변경
-
-
- ) : (
-
openArtworkModal(-1)} // -1은 썸네일을 의미
- style={{ cursor: 'pointer' }}
+ {/* 전시 썸네일 및 작품 등록 */}
+
+
전시 작품 등록
+
+ {/* 전시 썸네일 */}
+
+
+ {thumbnail ? (
+
+
+
openChangeArtworkModal(-1)}
>
-
-
- 전시 썸네일을 등록해주세요
- (필수)
-
-
- )}
-
+ 변경
+
+
+ ) : (
+
openArtworkModal(-1)} // -1은 썸네일
+ style={{ cursor: 'pointer' }}
+ >
+
+
+ 전시 썸네일을 등록해주세요
+ (필수)
+
+
+ )}
-
- {/* 전시 작품 슬라이드 (동적으로 생성) */}
- {renderArtworkSlides()}
-
- {/* 전시명 입력 */}
-
-
+ {/* 전시 작품 슬라이드 */}
+ {renderArtworkSlides()}
+
- {/* 전시 소개 입력 */}
-
-
-
+ {/* 전시명 입력 */}
+
+
+
- {/* 추가 기능 버튼들 */}
-
-
- 기간 설정하기 (필수)
- {exhibitionData.startDate && exhibitionData.endDate && (
-
- ✓ {exhibitionData.startDate.toLocaleDateString('ko-KR')} ~ {exhibitionData.endDate.toLocaleDateString('ko-KR')}
-
- )}
-
-
- {contactInfo.isRegistered ? '연락 정보 등록됨' : '연락 정보 등록하기'}
-
- navigate('/exhibition/offline-location')}
- >
- 오프라인 장소 등록하기
-
-
- 전시장 만들기
-
- navigate('/exhibition/participants', {
- state: {
- exhibitionData: {
- title: exhibitionData.title,
- description: exhibitionData.description,
- startDate: exhibitionData.startDate,
- endDate: exhibitionData.endDate,
- totalDays: exhibitionData.totalDays
- }
- }
- })}
- >
- {location.state?.participants ? '전시 참여자 등록됨' : '전시 참여자 등록하기'}
-
-
+ {/* 전시 소개 입력 */}
+
+
- {/* 하단 고정 전시 등록하기 버튼 */}
-
+ {/* 추가 기능 버튼들 */}
+
+
+ {isDateRangeSet() ? '기간 설정 완료' : '기간 설정하기 (필수)'}
+
+
+ navigate('/user/contact', {
+ state: {
+ exhibitionData: {
+ title: exhibitionData.title,
+ description: exhibitionData.description,
+ startDate: exhibitionData.startDate,
+ endDate: exhibitionData.endDate,
+ totalDays: exhibitionData.totalDays
+ },
+ thumbnail,
+ artworks
+ }
+ })}
+ >
+ {contactInfo.isRegistered ? '연락 정보 등록됨' : '연락 정보 등록하기'}
+
+
{
- // TODO: 전시 등록 로직 구현
- console.log('전시 등록:', exhibitionData);
- alert('전시 등록 기능은 아직 구현되지 않았습니다.');
- }}
+ type="button"
+ className={`${styles.featureButton} ${isOfflineLocationSet() ? styles.completed : ''}`}
+ onClick={() => navigate('/exhibition/offline-location', {
+ state: {
+ offlineLocation,
+ returnTo: 'exhibition-upload',
+ exhibitionData: {
+ title: exhibitionData.title,
+ description: exhibitionData.description,
+ startDate: exhibitionData.startDate,
+ endDate: exhibitionData.endDate,
+ totalDays: exhibitionData.totalDays
+ },
+ thumbnail,
+ artworks
+ }
+ })}
>
- 전시 등록하기
+ {isOfflineLocationSet() ? '오프라인 장소 등록됨' : '오프라인 장소 등록하기'}
+
+
+ 0 ? styles.completed : ''}`}
+ onClick={() => navigate('/exhibition/participants', {
+ state: {
+ participants,
+ returnTo: 'exhibition-upload',
+ exhibitionData: {
+ title: exhibitionData.title,
+ description: exhibitionData.description,
+ startDate: exhibitionData.startDate,
+ endDate: exhibitionData.endDate,
+ totalDays: exhibitionData.totalDays
+ },
+ thumbnail,
+ artworks
+ }
+ })}
+ >
+ {participants.length > 0 ? '전시 참여자 등록됨' : '전시 참여자 등록하기'}
+
+
+ {/* 하단 고정 전시 등록하기 버튼 */}
+
+
+ {isSubmitting ? '등록 중...' : '전시 등록하기'}
+
+
{/* 전시 작품 등록 모달 */}
{
+ try {
+ const response = await getPieceDraftCount();
+ console.log('getPieceDraftCount 응답:', response);
+ if (response?.success === true && response?.data) {
+ const count = response.data.count || response.data;
+ console.log('설정할 draftCount:', count);
+ setDraftCount(count);
+ } else {
+ console.log('응답이 성공이 아니거나 data가 없음');
+ setDraftCount(0);
+ }
+ } catch (error) {
+ console.error('임시저장 작품 개수 조회 실패:', error);
+ setDraftCount(0);
+ }
+ };
+
+ // 컴포넌트 마운트 시 작품 목록 초기화 및 draft 개수 확인
useEffect(() => {
- loadArtworks(true, 3);
- }, [loadArtworks]);
+ resetPieces();
+ fetchDraftCount();
+ }, [resetPieces]);
// 스크롤 이벤트 처리
useEffect(() => {
@@ -46,6 +75,15 @@ export default function MyArtworkPage() {
console.log('작품 클릭:', artwork);
};
+ // 작품 삭제 완료 시 작품 목록 새로고침
+ const handleArtworkDeleted = (deletedIds) => {
+ console.log('삭제된 작품 ID들:', deletedIds);
+ // 작품 목록을 새로고침
+ resetPieces();
+ // 임시저장 작품 개수도 새로고침
+ fetchDraftCount();
+ };
+
return (
{/* Status Bar 공간 */}
@@ -57,8 +95,13 @@ export default function MyArtworkPage() {
showAddButton={true}
onAddArtwork={handleAddArtwork}
onArtworkClick={handleArtworkClick}
- showDraftButton={hasDraft()}
+ showDraftButton={draftCount > 0}
onDraftClick={handleDraftClick}
+ artworks={artworks}
+ loading={loading}
+ hasMore={hasMore}
+ onLoadMore={loadMorePieces}
+ onArtworkDeleted={handleArtworkDeleted}
/>
);
diff --git a/src/components/museum/pages/MyExhibitionPage.jsx b/src/components/museum/pages/MyExhibitionPage.jsx
index fe7b485..ec063a2 100644
--- a/src/components/museum/pages/MyExhibitionPage.jsx
+++ b/src/components/museum/pages/MyExhibitionPage.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import ExhibitionList from '@museum/components/exhibition/ExhibitionList';
-import styles from './myExhibitionPage.module.css';
+import styles from './myArtworkPage.module.css';
export default function MyExhibitionPage() {
const navigate = useNavigate();
diff --git a/src/components/museum/pages/OfflineLocationPage.jsx b/src/components/museum/pages/OfflineLocationPage.jsx
index 12189de..d5ccae1 100644
--- a/src/components/museum/pages/OfflineLocationPage.jsx
+++ b/src/components/museum/pages/OfflineLocationPage.jsx
@@ -1,394 +1,87 @@
-import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
import chevronLeft from '@/assets/museum/chevron-left.png';
-import searchIcon from '@/assets/footer/search.svg';
import styles from './offlineLocationPage.module.css';
+import SearchBar from '@/components/feed/SearchBar';
export default function OfflineLocationPage() {
const navigate = useNavigate();
- const [currentStep, setCurrentStep] = useState(1); // 1: 검색, 2: 결과, 3: 소개입력
- const [searchQuery, setSearchQuery] = useState('');
- const [selectedLocation, setSelectedLocation] = useState(null);
+ const location = useLocation();
+ const [address, setAddress] = useState('');
const [exhibitionDescription, setExhibitionDescription] = useState('');
- const [searchResults, setSearchResults] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [isApiReady, setIsApiReady] = useState(false);
- const [apiLoadingMessage, setApiLoadingMessage] = useState('카카오맵 API 로딩 중...');
- // 카카오맵 API 스크립트 로드
- useEffect(() => {
- console.log('=== 카카오맵 API 초기화 시작 ===');
- console.trace('useEffect 호출 스택');
-
- // 이미 로드되어 있는지 확인
- if (window.kakao && window.kakao.maps && window.kakao.maps.services) {
- console.log('카카오맵 API가 이미 로드되어 있습니다.');
- console.log('기존 window.kakao 객체:', window.kakao);
- return;
- }
-
- console.log('기존 window.kakao 상태:', {
- 'window.kakao': !!window.kakao,
- 'window.kakao.maps': !!(window.kakao && window.kakao.maps),
- 'window.kakao.maps.services': !!(window.kakao && window.kakao.maps && window.kakao.maps.services)
- });
-
- const script = document.createElement('script');
- // 환경 변수에서 API 키를 가져오거나, 직접 입력
- const apiKey = import.meta.env.VITE_KAKAO_MAP_API_KEY || '7ad52f7b94094552a3513f9f7218363c';
-
- if (!apiKey || apiKey === 'YOUR_KAKAO_APP_KEY') {
- console.error('카카오맵 API 키가 설정되지 않았습니다.');
- alert('카카오맵 API 키를 설정해주세요.');
- return;
- }
-
- console.log('=== 스크립트 생성 정보 ===');
- console.log('API 키:', apiKey);
- console.log('스크립트 URL:', `//dapi.kakao.com/v2/maps/sdk.js?appkey=${apiKey}&libraries=services`);
- console.trace('스크립트 생성 위치');
-
- script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${apiKey}&libraries=services`;
- script.async = true;
-
- console.log('스크립트 태그 생성 완료:', script);
- console.log('스크립트 src:', script.src);
- console.log('스크립트 async:', script.async);
- console.log('스크립트 readyState:', script.readyState);
-
- script.onload = () => {
- console.log('=== 스크립트 onload 이벤트 발생 ===');
- console.trace('onload 콜백 스택');
- console.log('카카오맵 스크립트 로드 완료');
-
- // 카카오맵 SDK의 공식적인 초기화 완료 감지 사용
- if (window.kakao && window.kakao.maps) {
- console.log('카카오맵 기본 객체 확인됨');
- console.log('현재 readyState:', window.kakao.maps.readyState);
-
- // services 라이브러리가 로드되지 않은 경우 명시적으로 로드
- if (!window.kakao.maps.services || Object.keys(window.kakao.maps.services).length === 0) {
- console.log('services 라이브러리가 로드되지 않음, 명시적으로 로드 시도');
-
- // services.js 스크립트 직접 로드
- const servicesScript = document.createElement('script');
- servicesScript.src = '//t1.daumcdn.net/mapjsapi/js/libs/services/1.0.2/services.js';
- servicesScript.async = true;
-
- servicesScript.onload = () => {
- console.log('services.js 로드 완료');
- // 약간의 지연 후 services 객체 확인
- setTimeout(() => {
- if (window.kakao.maps.services && window.kakao.maps.services.Geocoder) {
- console.log('Geocoder 서비스 사용 가능 (명시적 로드 후)');
- setIsApiReady(true);
- setApiLoadingMessage('카카오맵 API 준비 완료!');
- } else {
- console.error('여전히 Geocoder 서비스를 찾을 수 없습니다');
- setApiLoadingMessage('Geocoder 서비스 로드 실패');
- }
- }, 500);
- };
-
- servicesScript.onerror = () => {
- console.error('services.js 로드 실패');
- setApiLoadingMessage('services 라이브러리 로드 실패');
- };
-
- document.head.appendChild(servicesScript);
- return;
- }
-
- // readyState가 2가 아니면 아직 로딩 중
- if (window.kakao.maps.readyState !== 2) {
- console.log('카카오맵 SDK 아직 로딩 중, load 콜백 등록');
-
- window.kakao.maps.load(() => {
- console.log('=== 카카오맵 SDK 로딩 완료 ===');
- console.log('최종 readyState:', window.kakao.maps.readyState);
- console.log('사용 가능한 서비스들:', Object.keys(window.kakao.maps.services || {}));
-
- if (window.kakao.maps.services && window.kakao.maps.services.Geocoder) {
- console.log('Geocoder 서비스 사용 가능');
- setIsApiReady(true);
- setApiLoadingMessage('카카오맵 API 준비 완료!');
- console.log('API 상태 업데이트: 준비 완료');
- } else {
- console.error('Geocoder 서비스를 찾을 수 없습니다');
- setApiLoadingMessage('Geocoder 서비스를 찾을 수 없습니다');
- }
- });
- } else {
- console.log('카카오맵 SDK 이미 로딩 완료됨');
- console.log('사용 가능한 서비스들:', Object.keys(window.kakao.maps.services || {}));
-
- if (window.kakao.maps.services && window.kakao.maps.services.Geocoder) {
- console.log('Geocoder 서비스 사용 가능 (이미 로딩됨)');
- setIsApiReady(true);
- setApiLoadingMessage('카카오맵 API 준비 완료!');
- } else {
- console.error('Geocoder 서비스를 찾을 수 없습니다 (이미 로딩됨)');
- setApiLoadingMessage('Geocoder 서비스를 찾을 수 없습니다');
- }
- }
- } else {
- console.error('카카오맵 기본 객체를 찾을 수 없습니다');
- }
- };
-
- script.onerror = (error) => {
- console.error('=== 카카오맵 API 스크립트 로드 실패 ===');
- console.trace('onerror 콜백 스택');
- console.error('에러 객체:', error);
- console.error('에러 타입:', error?.type);
- console.error('에러 타겟:', error?.target);
- console.error('에러 메시지:', error?.message);
- alert('지도 API 로드에 실패했습니다. API 키와 도메인 설정을 확인해주세요.');
- };
-
- console.log('=== 스크립트 DOM에 추가 ===');
- console.trace('DOM 추가 위치');
- document.head.appendChild(script);
- console.log('스크립트가 DOM에 추가되었습니다.');
- console.log('현재 head의 스크립트 태그들:', document.head.querySelectorAll('script[src*="kakao"]'));
- return () => {
- console.log('=== useEffect 정리 함수 실행 ===');
- if (document.head.contains(script)) {
- document.head.removeChild(script);
- console.log('스크립트가 DOM에서 제거되었습니다.');
- }
- };
- }, []);
const handleBack = () => {
- if (currentStep > 1) {
- setCurrentStep(currentStep - 1);
- } else {
- navigate('/exhibition/upload');
- }
- };
-
- const handleSearch = async () => {
- console.log('=== 검색 함수 호출 시작 ===');
- console.trace('handleSearch 호출 스택');
-
- if (!searchQuery.trim()) {
- console.log('검색어가 비어있습니다.');
- return;
- }
-
- console.log('검색어:', searchQuery);
- console.log('API 준비 상태:', isApiReady);
+ // 전시 데이터를 포함하여 뒤로 가기
+ const exhibitionData = location.state?.exhibitionData || {};
- // API 상태 확인
- if (!isApiReady) {
- console.error('=== API 상태 확인 실패 ===');
- console.trace('API 상태 확인 실패 위치');
- console.error('isApiReady:', isApiReady);
- alert('카카오맵 API가 아직 로드되지 않았습니다. 잠시만 기다려주세요.');
- return;
- }
-
- console.log('API 상태 확인 완료, 검색 시작');
- setIsLoading(true);
+ // Date 객체를 YYYY-MM-DD 문자열로 변환
+ const safeExhibitionData = {
+ ...exhibitionData,
+ startDate: exhibitionData.startDate instanceof Date
+ ? exhibitionData.startDate.toISOString().split('T')[0]
+ : exhibitionData.startDate,
+ endDate: exhibitionData.endDate instanceof Date
+ ? exhibitionData.endDate.toISOString().split('T')[0]
+ : exhibitionData.endDate
+ };
- try {
- // 카카오 주소 검색 API 사용
- const results = await searchAddress(searchQuery);
- console.log('검색 성공, 결과:', results);
- setSearchResults(results);
- setCurrentStep(2);
- } catch (error) {
- console.error('=== 주소 검색 실패 ===');
- console.trace('주소 검색 실패 위치');
- console.error('에러 객체:', error);
- console.error('에러 메시지:', error.message);
- console.error('에러 스택:', error.stack);
- alert(error.message || '주소 검색에 실패했습니다. 잠시 후 다시 시도해주세요.');
- } finally {
- setIsLoading(false);
- console.log('검색 완료, 로딩 상태 해제');
- }
- };
-
- // 카카오 주소 검색 함수
- const searchAddress = async (query) => {
- console.log('=== searchAddress 함수 호출 ===');
- console.trace('searchAddress 호출 스택');
- console.log('검색 쿼리:', query);
+ console.log('뒤로 가기 시 전달할 데이터:', {
+ original: exhibitionData,
+ converted: safeExhibitionData
+ });
- return new Promise((resolve, reject) => {
- try {
- console.log('=== Geocoder 서비스 사용 시작 ===');
- console.log('window.kakao.maps.services.Geocoder 생성 시도');
-
- if (!window.kakao.maps.services.Geocoder) {
- console.error('Geocoder 생성자 함수가 존재하지 않습니다.');
- console.log('window.kakao.maps.services의 사용 가능한 키들:', Object.keys(window.kakao.maps.services));
- reject(new Error('Geocoder 서비스를 사용할 수 없습니다.'));
- return;
+ navigate('/exhibition/upload', {
+ state: {
+ draft: {
+ exhibitionData: safeExhibitionData,
+ thumbnail: location.state?.thumbnail || null,
+ artworks: location.state?.artworks || [],
+ offlineLocation: location.state?.offlineLocation || null,
+ participants: location.state?.participants || [],
+ contactRegistered: location.state?.contactRegistered || false
}
-
- const geocoder = new window.kakao.maps.services.Geocoder();
- console.log('Geocoder 인스턴스 생성 성공:', geocoder);
-
- console.log('addressSearch 메서드 호출 시작');
- geocoder.addressSearch(query, (result, status) => {
- console.log('=== addressSearch 콜백 실행 ===');
- console.log('검색 결과:', result);
- console.log('검색 상태:', status);
- console.log('결과 타입:', typeof result);
- console.log('결과 길이:', Array.isArray(result) ? result.length : '배열이 아님');
-
- if (status === window.kakao.maps.services.Status.OK) {
- console.log('검색 성공, 결과 변환 시작');
- const addresses = result.map((item, index) => {
- console.log(`결과 ${index}번째 아이템:`, item);
- return {
- id: item.id,
- roadName: item.road_address?.address_name || item.address_name,
- address: item.address_name,
- buildingName: item.road_address?.building_name || '',
- zoneNumber: item.road_address?.zone_no || '',
- detailAddress: item.address_name,
- x: item.x,
- y: item.y
- };
- });
- console.log('변환된 주소:', addresses);
- resolve(addresses);
- } else {
- console.error('=== 검색 실패 ===');
- console.error('실패 상태:', status);
- console.error('실패 상태 타입:', typeof status);
- reject(new Error(`주소를 찾을 수 없습니다. (상태: ${status})`));
- }
- });
- } catch (error) {
- console.error('=== Geocoder 사용 중 오류 ===');
- console.trace('Geocoder 오류 위치');
- console.error('에러 객체:', error);
- console.error('에러 메시지:', error.message);
- console.error('에러 스택:', error.stack);
- reject(new Error(`주소 검색 중 오류가 발생했습니다: ${error.message}`));
}
});
};
- const handleLocationSelect = (location) => {
- setSelectedLocation(location);
- setCurrentStep(3);
- };
-
const handleComplete = () => {
- // 여기에 완료 로직 추가
+ // 오프라인 장소 등록 완료
console.log('오프라인 장소 등록 완료:', {
- location: selectedLocation,
- description: exhibitionDescription
+ address
+ });
+
+ // 전시 등록 페이지로 돌아가면서 오프라인 장소 데이터 전달
+ const exhibitionData = location.state?.exhibitionData || {};
+
+ navigate('/exhibition/upload', {
+ state: {
+ draft: {
+ exhibitionData,
+ thumbnail: location.state?.thumbnail || null,
+ artworks: location.state?.artworks || [],
+ offlineLocation: {
+ address,
+ addressName: address,
+ offlineDescription: address
+ },
+ participants: location.state?.participants || [],
+ contactRegistered: location.state?.contactRegistered || false
+ }
+ }
});
- navigate('/exhibition/upload');
};
- const renderSearchStep = () => (
- <>
-
-
-
setSearchQuery(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
- className={styles.searchField}
- disabled={!isApiReady}
- />
-
-
-
-
- {isLoading &&
검색 중...
}
-
-
-
-
입력 예시
-
-
도로명+ 건물번호
-
남대문로 9길 40
-
-
지역명(동/리) + 번지
-
중구 다동 155
-
-
지역명(동/리)+ 건물명
-
분당 주공
-
-
- >
- );
-
- const renderResultStep = () => (
-
-
- {searchResults.length > 0 ? (
- searchResults.map((location, index) => (
-
handleLocationSelect(location)}
- >
-
도로명
-
- {location.roadName || location.address}
-
-
지번
-
- {location.detailAddress}
-
- {location.buildingName && (
- <>
-
건물명
-
- {location.buildingName}
-
- >
- )}
-
- ))
- ) : (
-
- 검색 결과가 없습니다. 다른 키워드로 검색해보세요.
-
- )}
-
-
- );
-
- const renderDescriptionStep = () => (
-
-
-
도로명
-
{selectedLocation?.roadName || selectedLocation?.address}
-
지번
-
{selectedLocation?.detailAddress}
- {selectedLocation?.buildingName && (
- <>
-
건물명
-
{selectedLocation.buildingName}
- >
- )}
-
-
-
+ const renderAddressInput = () => (
+
@@ -410,23 +103,20 @@ export default function OfflineLocationPage() {
- {currentStep === 1 && renderSearchStep()}
- {currentStep === 2 && renderResultStep()}
- {currentStep === 3 && renderDescriptionStep()}
+
+ {renderAddressInput()}
{/* 하단 버튼 */}
- {currentStep === 3 && (
-
-
- 등록 완료하기
-
-
- )}
+
+
+ 등록 완료하기
+
+
);
}
diff --git a/src/components/museum/pages/SharedLibraryEntryPage.jsx b/src/components/museum/pages/SharedLibraryEntryPage.jsx
index 36679b3..222d067 100644
--- a/src/components/museum/pages/SharedLibraryEntryPage.jsx
+++ b/src/components/museum/pages/SharedLibraryEntryPage.jsx
@@ -1,7 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
-import useUserStore from '@/stores/userStore';
-import useExhibitionPhotoStore from '@museum/services/exhibitionPhotoStore';
import chevronLeft from '@/assets/museum/chevron-left.png';
import cameraIcon from '@/assets/user/camera.png';
import plusCircleIcon from '@/assets/museum/plus-circle.png';
@@ -11,20 +9,10 @@ import styles from './sharedLibraryEntryPage.module.css';
export default function SharedLibraryEntryPage() {
const navigate = useNavigate();
const location = useLocation();
- const { user } = useUserStore();
// 선택된 공유 라이브러리 정보
const { selectedLibrary } = location.state || {};
- // Zustand store 사용
- const {
- thumbnail,
- artworks,
- setThumbnail,
- addArtwork,
- updateArtwork,
- removeArtwork: removeArtworkFromStore
- } = useExhibitionPhotoStore();
// 로컬 상태로 관리 (공유 라이브러리 작품 정보)
const [sharedLibraryData, setSharedLibraryData] = useState({
diff --git a/src/components/museum/pages/SharedLibrarySelectionPage.jsx b/src/components/museum/pages/SharedLibrarySelectionPage.jsx
index e9e8e96..6f42ac8 100644
--- a/src/components/museum/pages/SharedLibrarySelectionPage.jsx
+++ b/src/components/museum/pages/SharedLibrarySelectionPage.jsx
@@ -2,11 +2,9 @@ import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import chevronLeft from '@/assets/museum/chevron-left.png';
import styles from './sharedLibrarySelectionPage.module.css';
-import useUserStore from '@/stores/userStore';
export default function SharedLibrarySelectionPage() {
const navigate = useNavigate();
- const { updateInvitation } = useUserStore();
// 상태 관리
const [sharedLibraries, setSharedLibraries] = useState([]);
@@ -42,12 +40,6 @@ export default function SharedLibrarySelectionPage() {
// TODO: API 호출로 라이브러리 입장 처리
- // userStore 상태 업데이트 - 작품 등록 단계로
- updateInvitation({
- hasSharedLibraryRequest: false, // 공유 라이브러리 선택 안내 숨김
- // 다음 단계는 작품 등록이므로 별도 상태 필요
- });
-
// 공유 라이브러리 입장 페이지로 이동
navigate('/exhibition/shared-library-entry', {
state: { selectedLibrary: library }
diff --git a/src/components/museum/pages/offlineLocationPage.module.css b/src/components/museum/pages/offlineLocationPage.module.css
index 2584b5e..5c87cab 100644
--- a/src/components/museum/pages/offlineLocationPage.module.css
+++ b/src/components/museum/pages/offlineLocationPage.module.css
@@ -240,7 +240,7 @@
.descriptionField {
width: 100%;
- height: 308px;
+ height: 80px;
padding: 18px;
border: none;
background: none;
@@ -290,3 +290,14 @@
.completeButton:hover:not(:disabled) {
background: #e55a00;
}
+
+.addressField {
+ display: grid;
+ grid-template-columns: 1fr 48px;
+ align-items: center;
+ width: 100%;
+ height: 48px;
+ background: var(--color-main);
+ border: 1px solid #000;
+ padding: 12px 20px;
+}
\ No newline at end of file
diff --git a/src/components/museum/services/artworkApi.js b/src/components/museum/services/artworkApi.js
deleted file mode 100644
index 41bc149..0000000
--- a/src/components/museum/services/artworkApi.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { fetchMyArtworks as fetchMyArtworksApi, deleteArtwork as deleteArtworkApi, updateArtworkStatus as updateArtworkStatusApi } from '@/apis/artwork';
-
-// 상태 텍스트 변환
-const getStatusText = (progressStatus) => ({
- REGISTERED: '등록 완료',
- ON_DISPLAY: '전시 중',
- WAITING: '등록 대기 중',
- UNREGISTERED: '등록 거절',
-}[progressStatus] || '알 수 없음');
-
-// 날짜 포맷
-const formatDate = (date) =>
- date.toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' })
- .replace(/\. /g, '.').replace('.', '');
-
-// API → 프론트 포맷 변환
-const transformArtworkData = (a) => ({
- id: a.pieceId,
- title: a.title,
- description: a.description,
- image: a.imageUrl,
- status: getStatusText(a.progressStatus),
- createdAt: formatDate(new Date()),
- isExhibiting: a.progressStatus === 'ON_DISPLAY',
- saveStatus: a.saveStatus,
- progressStatus: a.progressStatus,
- isPurchasable: a.isPurchasable,
- userId: a.userId,
-});
-
-// 내 작품 목록 조회
-export async function fetchMyArtworks(applicated = true, pageNum = 0, pageSize = 3) {
- try {
- const data = await fetchMyArtworksApi(applicated, pageNum, pageSize);
-
- // API 응답 구조에 따라 데이터 처리
- const d = data.data || data;
- return {
- content: (d.content || []).map(transformArtworkData),
- totalElements: d.totalElements,
- totalPages: d.totalPages,
- pageNum: d.pageNum,
- pageSize: d.pageSize,
- first: d.first,
- last: d.last,
- };
- } catch (error) {
- console.error('내 작품 조회 오류:', error);
- throw error;
- }
-}
-
-// 작품 삭제
-export async function deleteArtwork(pieceId) {
- try {
- const data = await deleteArtworkApi(pieceId);
- return data;
- } catch (error) {
- console.error('작품 삭제 오류:', error);
- throw error;
- }
-}
-
-// 작품 상태 업데이트
-export async function updateArtworkStatus(pieceId, status) {
- try {
- const data = await updateArtworkStatusApi(pieceId, status);
- return data;
- } catch (error) {
- console.error('작품 상태 업데이트 오류:', error);
- throw error;
- }
-}
diff --git a/src/components/museum/services/artworkDraftStore.js b/src/components/museum/services/artworkDraftStore.js
deleted file mode 100644
index 7ecffee..0000000
--- a/src/components/museum/services/artworkDraftStore.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import { create } from 'zustand';
-import { persist } from 'zustand/middleware';
-
-const useArtworkDraftStore = create(
- persist(
- (set, get) => ({
- // 여러 임시저장 데이터 배열
- drafts: [],
-
- // 임시저장 여부 확인
- hasDraft: () => {
- const { drafts } = get();
- return drafts.length > 0;
- },
-
- // 새 임시저장 추가
- saveDraft: (formData) => {
- const { drafts } = get();
-
- // 이미지 파일을 URL로 변환
- let imageUrl = null;
- if (formData.mainImage) {
- imageUrl = URL.createObjectURL(formData.mainImage);
- }
-
- const newDraft = {
- id: `draft_${Date.now()}`,
- title: formData.title || '',
- description: formData.description || '',
- image: imageUrl,
- mainImage: formData.mainImage, // 원본 파일도 저장
- detailImages: formData.detailImages || [],
- createdAt: (() => {
- const now = new Date();
- const year = String(now.getFullYear()).slice(-2);
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- return `${year}.${month}.${day}`;
- })()
- };
-
- const updatedDrafts = [...drafts, newDraft];
- set({ drafts: updatedDrafts });
-
- console.log('임시저장 완료:', newDraft);
- console.log('총 임시저장 개수:', updatedDrafts.length);
-
- return newDraft;
- },
-
- // 특정 임시저장 불러오기
- loadDraft: (draftId) => {
- const { drafts } = get();
- return drafts.find(draft => draft.id === draftId);
- },
-
- // 모든 임시저장 가져오기
- getAllDrafts: () => {
- const { drafts } = get();
- return drafts;
- },
-
- // 특정 임시저장 삭제
- deleteDraft: (draftId) => {
- const { drafts } = get();
- const draftToDelete = drafts.find(draft => draft.id === draftId);
-
- // 이미지 URL이 있다면 메모리에서 해제
- if (draftToDelete && draftToDelete.image && draftToDelete.image.startsWith('blob:')) {
- URL.revokeObjectURL(draftToDelete.image);
- }
-
- const updatedDrafts = drafts.filter(draft => draft.id !== draftId);
- set({ drafts: updatedDrafts });
-
- console.log('임시저장 삭제됨:', draftId);
- },
-
- // 모든 임시저장 삭제
- clearAllDrafts: () => {
- const { drafts } = get();
-
- // 모든 blob URL 메모리 해제
- drafts.forEach(draft => {
- if (draft.image && draft.image.startsWith('blob:')) {
- URL.revokeObjectURL(draft.image);
- }
- });
-
- set({ drafts: [] });
- console.log('모든 임시저장 삭제됨');
- }
- }),
- {
- name: 'artwork-drafts-storage', // localStorage key
- // 파일 객체는 직렬화할 수 없으므로 제외
- partialize: (state) => ({
- drafts: state.drafts.map(draft => ({
- id: draft.id,
- title: draft.title,
- description: draft.description,
- image: draft.image,
- createdAt: draft.createdAt
- }))
- })
- }
- )
-);
-
-export default useArtworkDraftStore;
diff --git a/src/components/museum/services/artworkStore.js b/src/components/museum/services/artworkStore.js
deleted file mode 100644
index 085f77f..0000000
--- a/src/components/museum/services/artworkStore.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { create } from 'zustand';
-import { fetchMyArtworks, deleteArtwork as deleteArtworkApi } from './artworkApi';
-
-const useArtworkStore = create((set, get) => ({
- artworks: [],
- layoutMode: 'vertical',
- isLoading: false,
- isLoadingMore: false,
- searchKeyword: '',
- applicated: true,
-
- pagination: {
- pageNum: 1,
- pageSize: 3,
- totalElements: 0,
- totalPages: 0,
- first: true,
- last: false,
- },
-
- error: null,
-
- setLayoutMode: (mode) => set({ layoutMode: mode }),
- setSearchKeyword: (keyword) => set({ searchKeyword: keyword }),
- setApplicated: (applicated) => set({ applicated }),
- setError: (error) => set({ error }),
- clearError: () => set({ error: null }),
-
- loadArtworks: async (reset = true, initialCount = 3) => {
- set({ isLoading: reset, isLoadingMore: !reset, error: null });
- try {
- const { applicated } = get();
- const pageNum = reset ? 0 : get().pagination.pageNum + 1;
- const data = await fetchMyArtworks(applicated, pageNum, initialCount);
-
- const nextState = {
- artworks: reset ? data.content : [...get().artworks, ...data.content],
- pagination: {
- pageNum: data.pageNum,
- pageSize: data.pageSize,
- totalElements: data.totalElements,
- totalPages: data.totalPages,
- first: data.first,
- last: data.last,
- },
- isLoading: false,
- isLoadingMore: false,
- };
- set(nextState);
- } catch (error) {
- const msg = String(error?.message || '');
- set({
- error: msg.includes('401') ? '인증이 만료되었습니다. 다시 로그인해주세요.' : (msg || '작품을 불러오는데 실패했습니다.'),
- isLoading: false,
- isLoadingMore: false,
- });
- }
- },
-
- loadMoreArtworks: async () => {
- const { isLoadingMore, isLoading, pagination, applicated } = get();
- if (isLoading || isLoadingMore || pagination.last) return;
-
- set({ isLoadingMore: true });
- try {
- const nextPage = pagination.pageNum + 1;
- const data = await fetchMyArtworks(applicated, nextPage, pagination.pageSize);
- set((state) => ({
- artworks: [...state.artworks, ...data.content],
- pagination: {
- pageNum: data.pageNum,
- pageSize: data.pageSize,
- totalElements: data.totalElements,
- totalPages: data.totalPages,
- first: data.first,
- last: data.last,
- },
- isLoadingMore: false,
- }));
- } catch (error) {
- const msg = String(error?.message || '');
- set({
- error: msg.includes('401') ? '인증이 만료되었습니다. 다시 로그인해주세요.' : (msg || '추가 작품을 불러오는데 실패했습니다.'),
- isLoadingMore: false,
- });
- }
- },
-
- refreshArtworks: async () => get().loadArtworks(true, 3),
-
- toggleApplicated: async () => {
- set({ applicated: !get().applicated });
- await get().loadArtworks(true, 3);
- },
-
- getFilteredArtworks: () => {
- const { artworks, searchKeyword } = get();
- if (!searchKeyword) return artworks;
- const q = searchKeyword.toLowerCase();
- return artworks.filter((a) =>
- (a.title || '').toLowerCase().includes(q) ||
- (a.description || '').toLowerCase().includes(q)
- );
- },
-
- getArtworkCount: () => get().pagination.totalElements,
- getExhibitingArtworks: () => get().artworks.filter((a) => a.isExhibiting),
- hasMore: () => !get().pagination.last,
-
- deleteArtwork: async (artworkId) => {
- try {
- await deleteArtworkApi(artworkId);
- set((state) => {
- const filtered = state.artworks.filter((a) => a.id !== artworkId);
- return {
- artworks: filtered,
- pagination: {
- ...state.pagination,
- totalElements: state.pagination.totalElements - 1,
- totalPages: Math.ceil((state.pagination.totalElements - 1) / state.pagination.pageSize),
- },
- };
- });
- return true;
- } catch (error) {
- set({ error: error.message || '작품 삭제에 실패했습니다.' });
- return false;
- }
- },
-
- updateArtworkStatus: (artworkId, newStatus) => {
- set((state) => ({
- artworks: state.artworks.map((a) =>
- a.id === artworkId
- ? { ...a, status: newStatus, isExhibiting: newStatus === '전시 중' }
- : a
- ),
- }));
- },
-}));
-
-export default useArtworkStore;
diff --git a/src/components/museum/services/exhibitionPhotoStore.js b/src/components/museum/services/exhibitionPhotoStore.js
deleted file mode 100644
index 61b8f21..0000000
--- a/src/components/museum/services/exhibitionPhotoStore.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { create } from 'zustand';
-
-const useExhibitionPhotoStore = create((set, get) => ({
- // 전시 썸네일
- thumbnail: null,
-
- // 전시 작품들 (배열로 관리)
- artworks: [],
-
- // 썸네일 설정
- setThumbnail: (file) => {
- set({ thumbnail: file });
- },
-
- // 썸네일 제거
- removeThumbnail: () => {
- set({ thumbnail: null });
- },
-
- // 작품 추가 (새로운 인덱스에 추가)
- addArtwork: (file) => {
- set((state) => ({
- artworks: [...state.artworks, file]
- }));
- },
-
- // 특정 인덱스의 작품 수정
- updateArtwork: (index, file) => {
- set((state) => {
- const newArtworks = [...state.artworks];
- newArtworks[index] = file;
- return { artworks: newArtworks };
- });
- },
-
- // 특정 인덱스의 작품 제거
- removeArtwork: (index) => {
- set((state) => ({
- artworks: state.artworks.filter((_, i) => i !== index)
- }));
- },
-
- // 모든 작품 제거
- clearArtworks: () => {
- set({ artworks: [] });
- },
-
- // 전체 상태 초기화
- reset: () => {
- set({ thumbnail: null, artworks: [] });
- },
-
- // 전체 상태 가져오기
- getExhibitionData: () => {
- const state = get();
- return {
- thumbnail: state.thumbnail,
- artworks: state.artworks
- };
- }
-}));
-
-export default useExhibitionPhotoStore;
diff --git a/src/components/user/ContactEditPage.jsx b/src/components/user/ContactEditPage.jsx
index 39fb977..a07b47f 100644
--- a/src/components/user/ContactEditPage.jsx
+++ b/src/components/user/ContactEditPage.jsx
@@ -1,14 +1,18 @@
import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import useUserStore from "@/stores/userStore";
+import { useNavigate, useLocation } from "react-router-dom";
+import { updateContact, getUserContact, getCurrentUser } from "@/apis/user/user.js";
import AppFooter from "@/components/footer/AppFooter";
import styles from "@/components/user/userEdit.module.css";
export default function ContactEditPage() {
const navigate = useNavigate();
- const { user, updateUser, updateContactInfo } = useUserStore();
+ const location = useLocation();
+ const [currentUserId, setCurrentUserId] = useState(null);
const [showCustomDomain, setShowCustomDomain] = useState(false);
+ // 전시 정보 상태 (전시 업로드 페이지에서 넘어온 경우)
+ const [exhibitionData, setExhibitionData] = useState(null);
+
// 폼 상태 관리
const [formData, setFormData] = useState({
emailUsername: "",
@@ -17,54 +21,108 @@ export default function ContactEditPage() {
instagram: ""
});
- // 컴포넌트 마운트 시 사용자 정보로 초기화
+ // 컴포넌트 마운트 시 사용자 ID 가져오기 및 연락 정보 불러오기
useEffect(() => {
- if (user.email) {
- const [username, domain] = user.email.split('@');
- setFormData(prev => ({
- ...prev,
- emailUsername: username || "",
- emailDomain: domain || "naver.com"
- }));
- }
- if (user.instagram) {
- setFormData(prev => ({
- ...prev,
- instagram: user.instagram
- }));
- }
- }, [user]);
-
- const handleCompleteClick = () => {
- // 이메일 주소 조합
- const finalEmail = showCustomDomain
- ? `${formData.emailUsername}@${formData.customDomain}`
- : `${formData.emailUsername}@${formData.emailDomain}`;
+ const fetchData = async () => {
+ try {
+ // 전시 정보가 있는지 확인 (전시 업로드 페이지에서 넘어온 경우)
+ if (location.state?.exhibitionData) {
+ console.log('ContactEditPage에서 받은 전시 데이터:', location.state);
+ setExhibitionData(location.state.exhibitionData);
+ }
+
+ // 현재 사용자 정보 조회하여 ID 가져오기
+ const userResponse = await getCurrentUser();
+ if (userResponse && userResponse.data && userResponse.data.userId) {
+ const userId = userResponse.data.userId;
+ setCurrentUserId(userId);
+
+ // 연락 정보 조회
+ const contactResponse = await getUserContact(userId);
+ console.log('연락 정보 API 응답:', contactResponse);
+
+ if (contactResponse && contactResponse.data) {
+ const contactData = contactResponse.data;
+
+ // 이메일 정보 설정
+ if (contactData.email) {
+ const [username, domain] = contactData.email.split('@');
+ setFormData(prev => ({
+ ...prev,
+ emailUsername: username || "",
+ emailDomain: domain || "naver.com"
+ }));
+ }
+
+ // 인스타그램 정보 설정
+ if (contactData.instagram) {
+ setFormData(prev => ({
+ ...prev,
+ instagram: contactData.instagram
+ }));
+ }
+ }
+ }
+ } catch (error) {
+ console.error('데이터 조회 실패:', error);
+ // 에러 발생 시 기본값 유지
+ }
+ };
+
+ fetchData();
+ }, [location.state]);
- console.log('저장할 연락정보:', {
- email: finalEmail,
- instagram: formData.instagram
- });
+ const handleCompleteClick = async () => {
+ try {
+ // 이메일 주소 조합
+ const finalEmail = showCustomDomain
+ ? `${formData.emailUsername}@${formData.customDomain}`
+ : `${formData.emailUsername}@${formData.emailDomain}`;
- // userStore 업데이트
- updateUser({
- email: finalEmail,
- instagram: formData.instagram
- });
+ console.log('저장할 연락정보:', {
+ email: finalEmail,
+ instagram: formData.instagram
+ });
- // 연락정보 등록 상태 업데이트
- updateContactInfo({
- isRegistered: true
- });
+ // API로 연락 정보 전송
+ const response = await updateContact({
+ email: finalEmail,
+ instagram: formData.instagram
+ });
- console.log('연락정보가 userStore에 저장되었습니다.');
+ console.log('연락 정보 등록 성공:', response);
- // 편집 완료 후 프로필 상세 페이지로 이동
- navigate('/user/profile');
+ // 편집 완료 후 전시 정보가 있으면 전시 업로드 페이지로, 없으면 프로필 페이지로 이동
+ if (location.state?.exhibitionData) {
+ navigate('/exhibition/upload', {
+ state: {
+ exhibitionData: location.state.exhibitionData,
+ thumbnail: location.state.thumbnail,
+ artworks: location.state.artworks,
+ contactUpdated: true
+ }
+ });
+ } else {
+ navigate('/user/profile');
+ }
+ } catch (error) {
+ console.error('연락 정보 등록 실패:', error);
+ alert('연락 정보 등록에 실패했습니다. 다시 시도해주세요.');
+ }
};
const handleBackClick = () => {
- navigate('/user/profile');
+ // 전시 등록 페이지에서 이동한 경우 전시 업로드 페이지로, 아니면 프로필 페이지로 이동
+ if (location.state?.exhibitionData) {
+ navigate('/exhibition/upload', {
+ state: {
+ exhibitionData: location.state.exhibitionData,
+ contactUpdated: false // 뒤로가기이므로 contactUpdated는 false
+ }
+ });
+ } else {
+ navigate('/user/profile');
+ }
};
// 입력 필드 변경 핸들러
@@ -101,7 +159,7 @@ export default function ContactEditPage() {
- 연락 정보 수정하기
+ 연락 정보 등록하기
완료
diff --git a/src/components/user/UserEditModal.jsx b/src/components/user/UserEditModal.jsx
index 1e06870..5022db5 100644
--- a/src/components/user/UserEditModal.jsx
+++ b/src/components/user/UserEditModal.jsx
@@ -56,7 +56,7 @@ export default function UserEditModal({ isOpen, onClose }) {
@@ -65,7 +65,7 @@ export default function UserEditModal({ isOpen, onClose }) {
diff --git a/src/components/user/UserEditPage.jsx b/src/components/user/UserEditPage.jsx
index 149e8be..d03854e 100644
--- a/src/components/user/UserEditPage.jsx
+++ b/src/components/user/UserEditPage.jsx
@@ -1,40 +1,107 @@
-import { useState, useCallback, useEffect } from "react";
+import { useState, useCallback, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
-import useUserStore from "@/stores/userStore";
+import { getCurrentUser, updateUserProfile, updateUserProfileImage, checkUserCode } from "@/apis/user/user.js";
import AppFooter from "@/components/footer/AppFooter";
import styles from "@/components/user/userEdit.module.css";
export default function UserEditPage() {
const navigate = useNavigate();
- const { user, updateUser } = useUserStore();
- const [userId, setUserId] = useState(user.nickname || "simonisnextdoor");
+ const fileInputRef = useRef(null);
+ const [user, setUser] = useState({
+ id: null,
+ name: '',
+ nickname: '',
+ introduction: '',
+ profileImageUrl: null,
+ code: null
+ });
+ const [userId, setUserId] = useState('');
const [userIdStatus, setUserIdStatus] = useState("available");
const [userIdMessage, setUserIdMessage] = useState("");
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [previewImage, setPreviewImage] = useState(null);
// 폼 상태 관리
const [formData, setFormData] = useState({
- name: user.name || "김땡땡",
- nickname: user.nickname || "simonisnextdoor",
- bio: ""
+ name: '',
+ nickname: '',
+ introduction: ''
});
- const handleCompleteClick = () => {
- // userStore 업데이트
- updateUser({
- name: formData.name,
- nickname: formData.nickname,
- bio: formData.bio
- });
+ // 이미지 선택 핸들러
+ const handleImageClick = () => {
+ fileInputRef.current?.click();
+ };
- // 편집 완료 후 프로필 상세 페이지로 이동
- navigate('/user/profile');
+ // 파일 선택 핸들러
+ const handleFileSelect = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ // 파일 유효성 검사
+ if (!file.type.startsWith('image/')) {
+ alert('이미지 파일만 선택할 수 있습니다.');
+ return;
+ }
+
+ if (file.size > 5 * 1024 * 1024) { // 5MB 제한
+ alert('파일 크기는 5MB 이하여야 합니다.');
+ return;
+ }
+
+ setSelectedImage(file);
+
+ // 미리보기 생성
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setPreviewImage(e.target.result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleCompleteClick = async () => {
+ // 아이디 유효성 검사
+ if (userIdStatus !== "available") {
+ alert('사용할 수 없는 아이디입니다. 다른 아이디를 입력해주세요.');
+ return;
+ }
+
+ try {
+ // 프로필 이미지 업데이트 (이미지가 선택된 경우)
+ if (selectedImage) {
+ try {
+ const imageResponse = await updateUserProfileImage(selectedImage);
+ } catch (error) {
+ console.error('프로필 이미지 업데이트 실패:', error);
+ console.error('에러 상세 정보:', {
+ message: error.message,
+ response: error.response?.data,
+ status: error.response?.status
+ });
+ alert(`프로필 이미지 업데이트에 실패했습니다.\n에러: ${error.message}`);
+ }
+ }
+
+ // API로 사용자 정보 업데이트
+ const response = await updateUserProfile({
+ nickname: formData.name, // 닉네임
+ code: formData.nickname, // 아이디 (code)
+ introduction: formData.introduction // 자기소개
+ });
+
+ // 편집 완료 후 프로필 상세 페이지로 이동
+ navigate('/user/profile');
+ } catch (error) {
+ console.error('사용자 정보 업데이트 실패:', error);
+ alert('프로필 수정에 실패했습니다. 다시 시도해주세요.');
+ }
};
const handleBackClick = () => {
navigate('/user/profile');
};
- // 아이디 중복 체크 함수 (실제로는 API 호출)
+ // 아이디 중복 체크 함수 (API 호출)
const checkUserIdAvailability = useCallback(async (id) => {
if (id.length < 3) {
setUserIdStatus("idle");
@@ -42,34 +109,59 @@ export default function UserEditPage() {
return;
}
+ if (id.length > 15) {
+ setUserIdStatus("unavailable");
+ setUserIdMessage("3~15자 이내로 입력해주세요");
+ return;
+ }
+
+ // 특수문자 체크 (영문, 숫자, 언더스코어, 점, 하이픈 허용)
+ const codeRegex = /^[a-zA-Z0-9_.-]+$/;
+ if (!codeRegex.test(id)) {
+ setUserIdStatus("unavailable");
+ setUserIdMessage("사용할 수 없는 특수문자가 포함되어 있습니다");
+ return;
+ }
+
+ // 현재 사용자의 code와 동일한 경우 중복 체크 건너뛰기
+ if (id === user.code) {
+ setUserIdStatus("available");
+ setUserIdMessage("현재 사용 중인 아이디입니다");
+ return;
+ }
+
setUserIdStatus("checking");
setUserIdMessage("확인 중...");
- // 실제 구현에서는 API 호출
- setTimeout(() => {
- // 임시 로직: 특정 아이디들을 이미 사용 중으로 설정
- const unavailableIds = ["admin", "test", "user", "guest"];
-
- if (unavailableIds.includes(id.toLowerCase())) {
- setUserIdStatus("unavailable");
- setUserIdMessage("이미 사용 중인 아이디입니다");
- } else {
- setUserIdStatus("available");
- setUserIdMessage("사용 가능한 아이디입니다");
+ try {
+ const response = await checkUserCode(id);
+ if (response && response.data !== undefined) {
+ // API 응답: data가 true면 중복, false면 사용 가능
+ if (response.data === false) {
+ setUserIdStatus("available");
+ setUserIdMessage("사용 가능한 아이디입니다");
+ } else {
+ setUserIdStatus("unavailable");
+ setUserIdMessage("이미 사용 중인 아이디입니다");
+ }
}
- }, 1000);
- }, []);
+ } catch (error) {
+ console.error('아이디 중복 확인 실패:', error);
+ setUserIdStatus("error");
+ setUserIdMessage("확인 중 오류가 발생했습니다");
+ }
+ }, [user.code]);
const handleUserIdChange = (e) => {
const newUserId = e.target.value;
setUserId(newUserId);
setFormData(prev => ({ ...prev, nickname: newUserId }));
- // 디바운싱을 위한 타이머
+ // 디바운싱을 위한 타이머 (1.3초 후 요청)
clearTimeout(window.userIdCheckTimer);
window.userIdCheckTimer = setTimeout(() => {
- checkUserIdAvailability(newUserId);
- }, 500);
+ checkUserIdAvailability(newUserId);
+ }, 1300);
};
// 입력 필드 변경 핸들러
@@ -77,9 +169,45 @@ export default function UserEditPage() {
setFormData(prev => ({ ...prev, [field]: value }));
};
+ // 컴포넌트 마운트 시 사용자 정보 불러오기
+ useEffect(() => {
+ const fetchUserData = async () => {
+ try {
+ const userResponse = await getCurrentUser();
+ console.log('사용자 정보 API 응답:', userResponse);
+
+ if (userResponse && userResponse.data) {
+ const userData = userResponse.data;
+ const newUser = {
+ id: userData.userId,
+ name: userData.nickname,
+ nickname: userData.code, // code를 nickname으로 매핑
+ introduction: userData.introduction || '',
+ profileImageUrl: userData.profileImageUrl,
+ code: userData.code
+ };
+
+ setUser(newUser);
+ setUserId(userData.code || ''); // code를 userId로 설정
+ setFormData({
+ name: userData.nickname || '', // nickname을 name으로
+ nickname: userData.code || '', // code를 nickname으로
+ introduction: userData.introduction || ''
+ });
+ }
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ }
+ };
+
+ fetchUserData();
+ }, []);
+
useEffect(() => {
// 초기 아이디 체크
- checkUserIdAvailability(userId);
+ if (userId) {
+ checkUserIdAvailability(userId);
+ }
}, [checkUserIdAvailability, userId]);
return (
@@ -100,9 +228,36 @@ export default function UserEditPage() {
{/* 프로필 사진 영역 */}
-
-
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleImageClick();
+ }
+ }}
+ >
+ {!previewImage && !user.profileImageUrl && (
+
+ )}
+
+ 사진 변경
+
+
+ {/* 숨겨진 파일 입력 */}
+
{/* 닉네임 영역 */}
@@ -139,7 +294,9 @@ export default function UserEditPage() {
{userIdMessage}
@@ -150,8 +307,8 @@ export default function UserEditPage() {
diff --git a/src/components/user/UserProfileDetailPage.jsx b/src/components/user/UserProfileDetailPage.jsx
index 4b86d43..e58da49 100644
--- a/src/components/user/UserProfileDetailPage.jsx
+++ b/src/components/user/UserProfileDetailPage.jsx
@@ -1,11 +1,54 @@
+import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
-import useUserStore from "@/stores/userStore";
+import { getCurrentUser, getContactStatus } from "@/apis/user/user.js";
import AppFooter from "@/components/footer/AppFooter";
import styles from "@/components/user/userProfileDetail.module.css";
export default function UserProfileDetailPage() {
const navigate = useNavigate();
- const { user, subscription } = useUserStore();
+ const [user, setUser] = useState({
+ id: null,
+ name: '',
+ nickname: '',
+ profileImageUrl: null,
+ code: null,
+ introduction: null
+ });
+ const [contactStatus, setContactStatus] = useState(false);
+
+ useEffect(() => {
+ // 페이지 로드 시 사용자 정보와 연락 정보 상태 가져오기
+ const fetchData = async () => {
+ try {
+ // 사용자 정보 조회
+ const userResponse = await getCurrentUser();
+ console.log('사용자 API 응답:', userResponse);
+
+ if (userResponse && userResponse.data) {
+ const userData = userResponse.data;
+ setUser({
+ id: userData.userId,
+ name: userData.nickname,
+ nickname: userData.nickname,
+ profileImageUrl: userData.profileImageUrl,
+ code: userData.code,
+ introduction: userData.introduction
+ });
+ }
+
+ // 연락 정보 상태 조회
+ const contactResponse = await getContactStatus();
+ console.log('연락 정보 API 응답:', contactResponse);
+ if (contactResponse && contactResponse.data !== undefined) {
+ setContactStatus(contactResponse.data);
+ }
+ } catch (error) {
+ console.error('데이터 조회 실패:', error);
+ }
+ };
+
+ fetchData();
+ }, []);
const handleEditClick = () => {
navigate('/user/edit');
@@ -37,37 +80,41 @@ export default function UserProfileDetailPage() {
{/* 프로필 사진 영역 */}
-
-
+ {console.log('현재 사용자 상태:', user)}
+
+ {!user.profileImageUrl && (
+
+ )}
{/* 닉네임과 태그 영역 */}
{user.name}
- @{user.nickname}
+ {user.code && (
+ @{user.code}
+ )}
- {user.bio || "안녕하세요. 아름다운 바다 그림을 통해 많은 사람들에게 행복을 주고 싶은 크리에이터입니다."}
+ {user.introduction || "자기소개를 등록해주세요."}
navigate('/user/contact')}
>
- 연락 정보 등록하기
+ {contactStatus ? '연락 정보 등록됨' : '연락 정보 등록하기'}
-
- {subscription.isPremium ? '멤버십 구독 중' : '멤버십 관리'}
-
-
+
diff --git a/src/components/user/UserProfileHeader.jsx b/src/components/user/UserProfileHeader.jsx
index b391bb3..c1b1f71 100644
--- a/src/components/user/UserProfileHeader.jsx
+++ b/src/components/user/UserProfileHeader.jsx
@@ -1,10 +1,38 @@
+import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
-import useUserStore from "../../stores/userStore";
+import { getCurrentUser } from "../../apis/user/user.js";
import styles from './userProfileHeader.module.css';
export default function UserProfileHeader() {
const navigate = useNavigate();
- const { user } = useUserStore();
+ const [user, setUser] = useState({
+ name: '',
+ code: '',
+ profileImageUrl: null
+ });
+
+ useEffect(() => {
+ // 컴포넌트 마운트 시 사용자 정보 가져오기
+ const fetchUserData = async () => {
+ try {
+ const response = await getCurrentUser();
+ console.log('UserProfileHeader - 사용자 API 응답:', response);
+
+ if (response && response.data) {
+ const userData = response.data;
+ setUser({
+ name: userData.nickname || '',
+ code: userData.code || '',
+ profileImageUrl: userData.profileImageUrl || null
+ });
+ }
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ }
+ };
+
+ fetchUserData();
+ }, []);
const handleProfileClick = () => {
navigate('/user/profile');
@@ -16,12 +44,12 @@ export default function UserProfileHeader() {
- {user.name}
- @{user.nickname}
+ {user.name}
+ {user.code && @{user.code} }
diff --git a/src/components/user/UserProfileModal.jsx b/src/components/user/UserProfileModal.jsx
index d3ee338..3820819 100644
--- a/src/components/user/UserProfileModal.jsx
+++ b/src/components/user/UserProfileModal.jsx
@@ -1,8 +1,26 @@
+import { useEffect } from "react";
import useUserStore from "../../stores/userStore";
+import { getCurrentUser } from "../../apis/user/user.js";
import styles from './userProfileModal.module.css';
export default function UserProfileModal({ isOpen, onClose, onEditClick }) {
- const { user, contactInfo, subscription } = useUserStore();
+ const { user, contactInfo, subscription, setUserFromAPI } = useUserStore();
+
+ useEffect(() => {
+ if (isOpen) {
+ // 모달이 열릴 때 사용자 정보 가져오기
+ const fetchUserData = async () => {
+ try {
+ const response = await getCurrentUser();
+ setUserFromAPI(response);
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ }
+ };
+
+ fetchUserData();
+ }
+ }, [isOpen, setUserFromAPI]);
if (!isOpen) return null;
diff --git a/src/components/user/userEdit.module.css b/src/components/user/userEdit.module.css
index f133a56..57763b0 100644
--- a/src/components/user/userEdit.module.css
+++ b/src/components/user/userEdit.module.css
@@ -66,11 +66,48 @@
height: 80px;
border-radius: 50%;
background-color: #eeeeee;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
aspect-ratio: 1/1;
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+}
+
+.profileDetailImage:hover {
+ transform: scale(1.05);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.imageOverlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.profileDetailImage:hover .imageOverlay {
+ opacity: 1;
+}
+
+.changeImageText {
+ color: white;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: center;
}
.cameraIcon {
@@ -346,3 +383,11 @@
.messageChecking {
color: #434343;
}
+
+.messageError {
+ color: #ff6b6b;
+}
+
+.messageIdle {
+ color: #757575;
+}
diff --git a/src/components/user/userProfileDetail.module.css b/src/components/user/userProfileDetail.module.css
index 62469e2..a19fa78 100644
--- a/src/components/user/userProfileDetail.module.css
+++ b/src/components/user/userProfileDetail.module.css
@@ -72,6 +72,9 @@
height: 80px;
border-radius: 50%;
background-color: #eeeeee;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
@@ -187,4 +190,4 @@
text-align: right;
margin-left: -30px;
margin-right: auto;
-}
+}
\ No newline at end of file
diff --git a/src/pages/ExhibitionDetailPage.jsx b/src/pages/ExhibitionDetailPage.jsx
index b11433b..f2cc62d 100644
--- a/src/pages/ExhibitionDetailPage.jsx
+++ b/src/pages/ExhibitionDetailPage.jsx
@@ -10,7 +10,7 @@ import ExhibitionVenue from '../components/exhibition/ExhibitionVenue';
import ExhibitionReviews from '../components/exhibition/ExhibitionReviews';
import ExhibitionParticipants from '../components/exhibition/ExhibitionParticipants';
import BackToTopButton from '../components/common/BackToTopButton';
-import { useExhibitionDetail, usePieceImages, useParticipantCreators, useExhibitionReviewsPreview } from '../apis/exhibition/exhibition';
+import { useExhibitionDetail, usePieceImages, useParticipantCreators, useExhibitionReviewsPreview } from '@apis/museum/exhibition';
import styles from './exhibitionDetailPage.module.css';
const ExhibitionDetailPage = () => {
diff --git a/src/pages/MuseumPage.jsx b/src/pages/MuseumPage.jsx
index fe9eab9..4b3fd18 100644
--- a/src/pages/MuseumPage.jsx
+++ b/src/pages/MuseumPage.jsx
@@ -7,43 +7,74 @@ import InsightSection from "@museum/components/museum/InsightSection";
import InvitationSection from "@museum/components/museum/InvitationSection";
import BackToTopButton from "@/components/common/BackToTopButton";
import AppFooter from "@/components/footer/AppFooter";
-import useUserStore from "@/stores/userStore";
-import { fetchMyArtworks } from "@/apis/artwork";
+import { getMyPieces } from "@apis/museum/artwork";
+import { getMyExhibitions } from "@apis/museum/exhibition";
+import { getCurrentUser } from "@/apis/user/user.js";
import styles from "@museum/components/museum/museum.module.css";
import commonStyles from "@museum/components/museum/common.module.css";
-// 이미지 import
-import exhibition1 from "@/assets/museum/큰사진1.png";
-import exhibition2 from "@/assets/museum/큰사진2.png";
export default function MuseumPage() {
- // Zustand에서 사용자 정보 가져오기
- const { user, subscription, invitation } = useUserStore();
-
// 작품 데이터 상태
const [artworks, setArtworks] = useState([]);
+ const [artworksTotal, setArtworksTotal] = useState(0);
+
+ // 전시회 데이터 상태
+ const [exhibitions, setExhibitions] = useState([]);
+ const [exhibitionsTotal, setExhibitionsTotal] = useState(0);
+
+ // 사용자 정보 상태
+ const [user, setUser] = useState(null);
+
+ // 초대 정보 상태
+ const [invitation, setInvitation] = useState({
+ hasInvitation: false,
+ hasSharedLibraryRequest: false,
+ invitationCount: 0
+ });
// 스크롤 상태 관리
const [isScrolled, setIsScrolled] = useState(false);
- // 작품 데이터 로드
+ // 데이터 로드
useEffect(() => {
- const loadArtworks = async () => {
+ const loadData = async () => {
try {
- console.log('작품 데이터 로드 시작...');
- const response = await fetchMyArtworks(true, 0, 10); // 등록 완료된 작품 10개
- console.log('API 응답:', response);
- console.log('응답 데이터:', response.data);
- console.log('작품 목록:', response.content);
+ // 사용자 정보 로드
+ const userResponse = await getCurrentUser();
+ if (userResponse?.data) {
+ setUser(userResponse.data);
+ console.log('MuseumPage - 사용자 정보:', userResponse.data);
+ }
+
+ // 작품 데이터 로드
+ const artworksResponse = await getMyPieces({ applicated: true, pageNum: 1, pageSize: 3 });
+ if (artworksResponse?.data) {
+ setArtworks(artworksResponse.data.content || []);
+ setArtworksTotal(artworksResponse.data.totalElements || 0);
+ console.log('설정된 작품 목록:', artworksResponse.data.content);
+ console.log('작품 총 개수:', artworksResponse.data.totalElements);
+ }
+
+ // 전시회 데이터 로드
+ const exhibitionsResponse = await getMyExhibitions({ pageNum: 1, pageSize: 3, fillAll: true });
+ console.log('전시회 API 응답:', exhibitionsResponse);
+ if (exhibitionsResponse?.data?.data) {
+ const content = exhibitionsResponse.data.data.content || [];
+ const totalElements = exhibitionsResponse.data.data.totalElements || 0;
+
+ setExhibitions(content);
+ setExhibitionsTotal(totalElements);
+
+ console.log('전시회 데이터 설정:', { content, totalElements });
+ }
- setArtworks(response.content || []);
- console.log('설정된 작품 목록:', response.content || []);
} catch (error) {
- console.error('작품 로드 오류:', error);
+ console.error('데이터 로드 오류:', error);
}
};
- loadArtworks();
+ loadData();
}, []);
// 스크롤 이벤트 처리
@@ -57,51 +88,13 @@ export default function MuseumPage() {
return () => window.removeEventListener('scroll', handleScroll);
}, []);
- const exhibitions = [
- {
- id: 1,
- title: "김땡땡 개인전 : 두 번째 여름",
- date: "24.12.5 - 25.2.19",
- image: exhibition1
- },
- {
- id: 2,
- title: "김땡땡 개인전",
- date: "24.12.5 - 25.2.19",
- image: exhibition2
- },
- {
- id: 3,
- title: "추상 미술의 세계",
- date: "24.11.1 - 25.1.15",
- image: exhibition1
- },
- {
- id: 4,
- title: "도시 풍경 전시",
- date: "24.10.20 - 24.12.30",
- image: exhibition2
- },
- {
- id: 5,
- title: "자연과 빛의 조화",
- date: "24.9.15 - 24.11.30",
- image: exhibition1
- },
- {
- id: 6,
- title: "현대 미술의 흐름",
- date: "24.8.1 - 24.10.15",
- image: exhibition2
- }
- ];
return (
-
+ {user &&
}
{/* 공동 전시 초대가 온 사용자에게만 보여주는 섹션 */}
@@ -114,10 +107,12 @@ export default function MuseumPage() {
)}
-
-
+
+
+
+
diff --git a/src/stores/userStore.js b/src/stores/userStore.js
index ad34bf3..fa24641 100644
--- a/src/stores/userStore.js
+++ b/src/stores/userStore.js
@@ -1,22 +1,9 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
-import profileImage from '@/assets/museum/프사.png'
const useUserStore = create(
persist(
(set) => ({
- // 사용자 정보
- user: {
- id: 1,
- name: '김땡땡',
- nickname: 'simonisnextdoor',
- email: 'asd123@naver.com',
- profileImage: profileImage,
- title: '크리에이터의 전시장',
- bio: '',
- instagram: 'simonisnextdoor'
- },
-
// 사용자 선호도
preferences: {
exhibitions: [],
@@ -26,68 +13,6 @@ const useUserStore = create(
nickname: '',
userId: '',
},
-
- // 로그인 상태
- isLoggedIn: true,
-
- // 액세스 토큰 (API 호출용)
- accessToken: 'your-access-token-here', // 실제 토큰으로 교체 필요
-
- // 구독 정보
- subscription: {
- isPremium: false, // 구독 상태별 조건부 렌더링 확인
- plan: 'premium', // 'free', 'premium', 'pro'
- startDate: new Date().toISOString(),
- endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
- },
-
- // 연락 정보
- contactInfo: {
- isRegistered: true, // 연락 정보 등록 여부
- accountNumber: '1234567890',
- bankName: '국민은행',
- },
-
- // 공동 전시 초대 관련 상태
- invitation: {
- hasInvitation: true, // 공동 전시 참여 요청 여부
- hasSharedLibraryRequest: false, // 작품 공유 라이브러리 등록 필요 여부
- invitationCount: 1, // 참여 요청 개수
- },
-
- // 구독 상태 업데이트 (테스트용)
- updateSubscription: (subscriptionData) => {
- set(state => ({
- subscription: { ...state.subscription, ...subscriptionData }
- }))
- },
-
- // 연락 정보 업데이트
- updateContactInfo: (contactData) => {
- set(state => ({
- contactInfo: { ...state.contactInfo, ...contactData }
- }))
- },
-
- // 사용자 정보 업데이트
- updateUser: (userData) => {
- set(state => ({
- user: { ...state.user, ...userData }
-
- }))
- },
-
- // 공동 전시 초대 상태 업데이트
- updateInvitation: (invitationData) => {
- set(state => ({
- invitation: { ...state.invitation, ...invitationData }
- }))
- },
-
- // 액세스 토큰 업데이트
- setAccessToken: (token) => {
- set({ accessToken: token })
- },
// 전시 선호도 업데이트
updateExhibitionPreferences: (exhibitions) => {
@@ -106,13 +31,7 @@ const useUserStore = create(
{
name: 'artium-user-storage',
partialize: (state) => ({
- user: state.user,
- isLoggedIn: state.isLoggedIn,
- accessToken: state.accessToken,
- preferences: state.preferences,
- subscription: state.subscription,
- contactInfo: state.contactInfo,
- invitation: state.invitation
+ preferences: state.preferences
})
}
)
diff --git a/vite.config.js b/vite.config.js
index 1534215..23af6a8 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -7,7 +7,7 @@ export default defineConfig({
resolve: {
alias: [
{ find: '@', replacement: '/src' },
- { find: '@api', replacement: '/src/api' },
+ { find: '@apis', replacement: '/src/apis' },
{ find: '@components', replacement: '/src/components' },
{ find: '@commons', replacement: '/src/components/commons' },
{ find: '@pages', replacement: '/src/pages' },
@@ -18,6 +18,8 @@ export default defineConfig({
],
},
server: {
+ host: '0.0.0.0',
+ port: 5173,
proxy: {
'/api': {
target: 'https://api.artium.life',
From 26d93ae98f8346d82af488cc34d56395576802ef Mon Sep 17 00:00:00 2001
From: wnsgur393 <2021301022@skuniv.ac.kr>
Date: Mon, 25 Aug 2025 03:00:47 +0900
Subject: [PATCH 5/6] =?UTF-8?q?api=20=EC=97=B0=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/museum/exhibition.js | 42 +--
src/apis/user/user.js | 2 +-
.../exhibition/ExhibitionArtworkModal.jsx | 26 +-
.../exhibition/ExhibitionDatePicker.jsx | 3 +-
.../museum/pages/ArtworkLibraryPage.jsx | 44 ++-
.../museum/pages/ArtworkUploadPage.jsx | 30 ++-
.../pages/ExhibitionParticipantPage.jsx | 26 +-
.../museum/pages/ExhibitionUploadPage.jsx | 253 ++++++++++--------
.../museum/pages/OfflineLocationPage.jsx | 138 +++++-----
.../museum/pages/SharedLibraryEntryPage.jsx | 154 ++++++++++-
.../pages/offlineLocationPage.module.css | 15 +-
src/components/user/ContactEditPage.jsx | 33 ++-
vite.config.js | 13 +-
13 files changed, 519 insertions(+), 260 deletions(-)
diff --git a/src/apis/museum/exhibition.js b/src/apis/museum/exhibition.js
index 88bb5a3..0a77c7a 100644
--- a/src/apis/museum/exhibition.js
+++ b/src/apis/museum/exhibition.js
@@ -2,26 +2,32 @@ import { useState, useEffect, useCallback } from 'react';
import APIService from '../axios';
/**
- * 전시 등록 API
- * @param {Object} exhibitionData - 전시 등록 데이터
- * @param {number[]} exhibitionData.pieceIdList - 작품 ID 목록
- * @param {string} exhibitionData.endDate - 종료일 (YYYY-MM-DD)
- * @param {number[]} exhibitionData.participantIdList - 참여자 ID 목록
- * @param {string} exhibitionData.startDate - 시작일 (YYYY-MM-DD)
- * @param {string} exhibitionData.address - 주소
- * @param {string} exhibitionData.title - 전시 제목
- * @param {string} exhibitionData.offlineDescription - 오프라인 전시 설명
- * @param {string} exhibitionData.description - 전시 설명
- * @param {string} exhibitionData.addressName - 주소명
- * @returns {Promise} 전시 등록 결과
+ * 전시 등록 API (multipart/form-data)
+ * @param {Object} exhibitionData - DTO 그대로 JSON으로 보낼 데이터 (Swagger의 request object와 동일)
+ * @param {File=} imageFile - 썸네일 파일(선택). 없으면 생략 가능
+ * @returns {Promise}
*/
-export const createExhibition = async (exhibitionData) => {
+export const createExhibition = async (exhibitionData, imageFile) => {
try {
- const response = await APIService.private.post('/api/exhibitions', exhibitionData);
- return response;
- } catch (error) {
- console.error('전시 등록 실패:', error);
- throw error;
+ const formData = new FormData();
+
+ // ✅ 서버가 기대하는 "request" 파트에 JSON 통째로 담기
+ formData.append(
+ "request",
+ new Blob([JSON.stringify(exhibitionData)], { type: "application/json" })
+ );
+
+ // ✅ 이미지 파일 파트명은 Swagger의 "image"와 동일해야 함
+ if (imageFile) {
+ formData.append("image", imageFile);
+ }
+
+ // axios는 boundary 자동 설정
+ const res = await APIService.private.post("/api/exhibitions", formData);
+ return res;
+ } catch (err) {
+ console.error("전시 등록 실패:", err);
+ throw err;
}
};
diff --git a/src/apis/user/user.js b/src/apis/user/user.js
index b3b6dd2..b8e8bb5 100644
--- a/src/apis/user/user.js
+++ b/src/apis/user/user.js
@@ -108,7 +108,7 @@ export const deleteUserAccount = async () => {
*/
export const getUserProfilesByCode = async (userCode) => {
try {
- const response = await APIService.private.get(`/api/users/search?keyword=${userCode}`);
+ const response = await APIService.private.get(`/api/users/profile?code=${userCode}`);
return response;
} catch (error) {
console.error('사용자 프로필 조회 실패:', error);
diff --git a/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx b/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
index 557f7b9..d67cac7 100644
--- a/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionArtworkModal.jsx
@@ -4,10 +4,10 @@ import styles from './exhibitionArtworkModal.module.css';
export default function ExhibitionArtworkModal({
isOpen,
onClose,
- onNewArtwork,
- onLoadFromLibrary,
isThumbnail = false,
- isChangeMode = false
+ isChangeMode = false,
+ currentDraft = null,
+ returnTo = 'exhibition-upload'
}) {
const navigate = useNavigate();
@@ -20,22 +20,30 @@ export default function ExhibitionArtworkModal({
};
const handleNewArtwork = () => {
- // 새 작품 등록 페이지로 이동
+ // 새 작품 등록 페이지로 이동하면서 draft 데이터 전달
navigate('/artwork/upload', {
state: {
fromExhibition: true,
- currentArtworkIndex: isThumbnail ? -1 : 0,
+ isThumbnail,
isChangeMode,
- returnTo: 'exhibition-upload'
+ returnTo,
+ draft: currentDraft
}
});
onClose();
};
const handleLoadFromLibrary = () => {
- if (onLoadFromLibrary) {
- onLoadFromLibrary();
- }
+ // 작품 라이브러리 페이지로 이동하면서 draft 데이터 전달
+ navigate('/artwork/library', {
+ state: {
+ fromExhibition: true,
+ isThumbnail,
+ isChangeMode,
+ returnTo,
+ draft: currentDraft
+ }
+ });
onClose();
};
diff --git a/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx b/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
index 2c08b68..a8f57c6 100644
--- a/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
+++ b/src/components/museum/components/exhibition/ExhibitionDatePicker.jsx
@@ -177,8 +177,7 @@ export default function ExhibitionDatePicker() {
totalDays: getTotalDays()
};
- console.log('ExhibitionDatePicker - 날짜 데이터가 포함된 draft:', updatedDraft);
- console.log('변환된 날짜:', { startDate: startDateString, endDate: endDateString });
+
// 업로드 페이지로 돌아가면서 선택된 날짜 + draft 같이 전달
// 업로드 페이지에서 location.state.selectedDates & .draft를 사용해 복원
diff --git a/src/components/museum/pages/ArtworkLibraryPage.jsx b/src/components/museum/pages/ArtworkLibraryPage.jsx
index 004a707..07916b1 100644
--- a/src/components/museum/pages/ArtworkLibraryPage.jsx
+++ b/src/components/museum/pages/ArtworkLibraryPage.jsx
@@ -10,7 +10,7 @@ export default function ArtworkLibraryPage() {
const location = useLocation();
// 전시 등록 페이지에서 전달받은 정보
- const { fromExhibition, artworkIndex, isThumbnail, isChangeMode } = location.state || {};
+ const { fromExhibition, artworkIndex, isThumbnail, isChangeMode, draft, returnTo } = location.state || {};
// API를 사용한 작품 목록 관리
const {
@@ -52,8 +52,15 @@ export default function ArtworkLibraryPage() {
}, [loading, hasMore, loadMorePieces]);
const handleBack = () => {
- if (fromExhibition) {
- // 전시 등록 페이지로 돌아가기
+ if (fromExhibition && returnTo === 'exhibition-upload') {
+ // 전시 등록 페이지로 돌아가면서 draft 데이터 전달
+ navigate('/exhibition/upload', {
+ state: {
+ draft: draft
+ }
+ });
+ } else if (fromExhibition) {
+ // 전시 등록 페이지로 돌아가기 (기존 방식)
navigate('/exhibition/upload', {
state: {
returnFromLibrary: true,
@@ -100,15 +107,28 @@ export default function ArtworkLibraryPage() {
);
if (fromExhibition && selectedArtworkList.length > 0) {
- // 전시 등록 페이지로 돌아가면서 선택된 작품 정보 전달
- navigate('/exhibition/upload', {
- state: {
- returnFromLibrary: true,
- selectedArtworks: selectedArtworkList, // 선택된 모든 작품 배열
- artworkIndex: artworkIndex,
- isThumbnail: isThumbnail
- }
- });
+ if (returnTo === 'exhibition-upload') {
+ // draft 데이터와 함께 선택된 작품 정보 전달
+ navigate('/exhibition/upload', {
+ state: {
+ draft: draft,
+ returnFromLibrary: true,
+ selectedArtworks: selectedArtworkList, // 선택된 모든 작품 배열
+ artworkIndex: artworkIndex,
+ isThumbnail: isThumbnail
+ }
+ });
+ } else {
+ // 기존 방식 (하위 호환성)
+ navigate('/exhibition/upload', {
+ state: {
+ returnFromLibrary: true,
+ selectedArtworks: selectedArtworkList, // 선택된 모든 작품 배열
+ artworkIndex: artworkIndex,
+ isThumbnail: isThumbnail
+ }
+ });
+ }
}
};
diff --git a/src/components/museum/pages/ArtworkUploadPage.jsx b/src/components/museum/pages/ArtworkUploadPage.jsx
index e0b850e..dd19cf2 100644
--- a/src/components/museum/pages/ArtworkUploadPage.jsx
+++ b/src/components/museum/pages/ArtworkUploadPage.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
import { createPiece } from '@apis/museum/artwork';
import { getCurrentUser } from '@apis/user/user';
import ArtworkModal from '@museum/components/artwork/ArtworkModal';
@@ -10,6 +10,14 @@ import styles from './artworkUploadPage.module.css';
export default function ArtworkUploadPage() {
const navigate = useNavigate();
+ const location = useLocation();
+
+ // 전시 등록에서 넘어온 경우 draft 데이터 받기
+ const draft = location.state?.draft;
+ const fromExhibition = location.state?.fromExhibition;
+ const isThumbnail = location.state?.isThumbnail;
+ const isChangeMode = location.state?.isChangeMode;
+ const returnTo = location.state?.returnTo;
const [user, setUser] = useState(null);
const [formData, setFormData] = useState({
title: '',
@@ -157,7 +165,14 @@ export default function ArtworkUploadPage() {
if (formData.title || formData.description || formData.mainImage || formData.detailImages.length > 0) {
setModal({ isOpen: true, type: 'cancel' });
} else {
- navigate('/artwork/my');
+ // 전시 등록에서 넘어온 경우 draft 데이터와 함께 돌아가기
+ if (fromExhibition && returnTo === 'exhibition-upload') {
+ navigate('/exhibition/upload', {
+ state: { draft: draft }
+ });
+ } else {
+ navigate('/artwork/my');
+ }
}
};
@@ -255,8 +270,15 @@ export default function ArtworkUploadPage() {
// 등록 완료시 모달 닫고 페이지 이동
setModal({ isOpen: false, type: null });
- // 내 작품 페이지로 바로 이동
- navigate('/artwork/my');
+ // 전시 등록에서 넘어온 경우 draft 데이터와 함께 돌아가기
+ if (fromExhibition && returnTo === 'exhibition-upload') {
+ navigate('/exhibition/upload', {
+ state: { draft: draft }
+ });
+ } else {
+ // 내 작품 페이지로 바로 이동
+ navigate('/artwork/my');
+ }
};
const handleCancelConfirm = () => {
diff --git a/src/components/museum/pages/ExhibitionParticipantPage.jsx b/src/components/museum/pages/ExhibitionParticipantPage.jsx
index 7738182..79ad2f5 100644
--- a/src/components/museum/pages/ExhibitionParticipantPage.jsx
+++ b/src/components/museum/pages/ExhibitionParticipantPage.jsx
@@ -16,8 +16,9 @@ export default function ExhibitionParticipantPage() {
const [isSearching, setIsSearching] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false);
- // URL state에서 전시 정보 받아오기
- const exhibitionData = location.state?.exhibitionData || {};
+ // URL state에서 draft 정보 받아오기
+ const draft = location.state?.draft || {};
+ const exhibitionData = draft.exhibitionData || {};
// 사용자 검색 함수
const searchUsers = async (query) => {
@@ -29,12 +30,10 @@ export default function ExhibitionParticipantPage() {
setIsSearching(true);
try {
- // @를 앞에 붙여서 API 요청
- const userCode = `@${query.trim()}`;
- console.log('사용자 검색 요청:', userCode);
+ // 검색어를 그대로 사용
+ const keyword = query.trim();
- const response = await getUserProfilesByCode(userCode);
- console.log('사용자 검색 응답:', response);
+ const response = await getUserProfilesByCode(keyword);
if (response && response.data && Array.isArray(response.data)) {
setSearchResults(response.data);
@@ -90,11 +89,13 @@ export default function ExhibitionParticipantPage() {
// 2초 후 화면 전환
setTimeout(() => {
- // 전시 업로드 페이지로 돌아가면서 선택된 참여자 정보 전달
+ // 전시 업로드 페이지로 돌아가면서 전체 draft 정보 전달
navigate('/exhibition/upload', {
state: {
- participants: selectedParticipants,
- exhibitionData
+ draft: {
+ ...draft, // 기존 draft 데이터 모두 포함
+ participants: selectedParticipants // 참여자 정보 추가/업데이트
+ }
}
});
}, 2000);
@@ -135,7 +136,7 @@ export default function ExhibitionParticipantPage() {
{/* 안내 메시지 */}
- {isSearching && searchResults.length > 0 && (
+ {searchResults.length > 0 && (
{selectedParticipants.length === 0
? '프로필을 누르면 등록이 완료됩니다'
@@ -145,7 +146,8 @@ export default function ExhibitionParticipantPage() {
)}
{/* 검색 결과 목록 */}
- {isSearching && searchResults.length > 0 && (
+ {console.log('렌더링 조건 확인:', { isSearching, searchResultsLength: searchResults.length, searchResults })}
+ {searchResults.length > 0 && (
{searchResults.map((user) => (
{
const { startDate, endDate } = exhibitionData;
- console.log('isDateRangeSet 호출됨:', {
- startDate,
- endDate,
- startDateType: typeof startDate,
- endDateType: typeof endDate,
- startDateIsDate: startDate instanceof Date,
- endDateIsDate: endDate instanceof Date
- });
-
// Date 객체인 경우
if (startDate instanceof Date && endDate instanceof Date) {
- const result = !isNaN(startDate.getTime()) && !isNaN(endDate.getTime());
- console.log('Date 객체 처리 결과:', result);
- return result;
+ return !isNaN(startDate.getTime()) && !isNaN(endDate.getTime());
}
// 문자열인 경우 (YYYY-MM-DD 형식)
if (typeof startDate === 'string' && typeof endDate === 'string') {
const startPattern = /^\d{4}-\d{2}-\d{2}$/;
const endPattern = /^\d{4}-\d{2}-\d{2}$/;
- const startValid = startPattern.test(startDate);
- const endValid = endPattern.test(endDate);
- const result = startValid && endValid;
-
- console.log('문자열 처리 결과:', {
- startValid,
- endValid,
- startPattern: startPattern.test(startDate),
- endPattern: endPattern.test(endDate),
- result
- });
-
- return result;
+ return startPattern.test(startDate) && endPattern.test(endDate);
}
- console.log('조건에 맞지 않음, false 반환');
return false;
};
// 오프라인 장소 등록 완료 상태 확인 함수
const isOfflineLocationSet = () => {
- const { address, addressName, offlineDescription } = offlineLocation;
-
- console.log('isOfflineLocationSet 호출됨:', {
- address,
- addressName,
- offlineDescription,
- addressType: typeof address,
- addressNameType: typeof addressName,
- offlineDescriptionType: typeof offlineDescription,
- addressValid: address && address.trim().length > 0,
- addressNameValid: addressName && addressName.trim().length > 0,
- offlineDescriptionValid: offlineDescription && offlineDescription.trim().length > 0
- });
-
- const result = address && address.trim().length > 0;
- console.log('오프라인 장소 설정 결과:', result);
- return result;
+ const { address, addressName } = offlineLocation;
+ return address && address.trim().length > 0 && addressName && addressName.trim().length > 0;
};
// 현재 입력 상태를 하나로 묶어 라우팅 state로 넘길 draft
const buildDraft = () => {
+ // artworks에서 pieceId만 추출하여 리스트 구성
+ const pieceIds = artworks.map(artwork => artwork.pieceId || artwork.id).filter(Boolean);
+
const draft = {
exhibitionData,
thumbnail,
artworks,
+ pieceIds, // pieceId 리스트 추가
offlineLocation,
participants,
contactRegistered: contactInfo.isRegistered,
@@ -124,21 +89,15 @@ export default function ExhibitionUploadPage() {
totalDays: exhibitionData.totalDays
};
- console.log('buildDraft 호출됨:', draft);
- console.log('날짜 형식 확인 - startDate:', draft.startDate, 'endDate:', draft.endDate);
return draft;
};
// 기간 설정 페이지에서 돌아왔을 때 데이터 복원 (통합)
useEffect(() => {
- console.log('useEffect 실행 - location.state:', location.state);
-
// location.state가 실제로 유효한 데이터를 가지고 있을 때만 처리
if (location.state && (location.state.selectedDates || location.state.draft)) {
const { selectedDates, draft } = location.state;
- console.log('데이터 복원 시작:', { selectedDates, draft });
-
setExhibitionData(prev => {
let newData = { ...prev };
@@ -175,12 +134,6 @@ export default function ExhibitionUploadPage() {
if (!selectedDates && draft) {
// draft에서 직접 날짜 데이터 복원
if (draft.startDate && draft.endDate) {
- console.log('draft에서 날짜 데이터 복원:', {
- startDate: draft.startDate,
- endDate: draft.endDate,
- totalDays: draft.totalDays
- });
-
newData = {
...newData,
startDate: draft.startDate,
@@ -191,12 +144,6 @@ export default function ExhibitionUploadPage() {
// draft.exhibitionData에서도 날짜 데이터 확인 및 복원
if (draft.exhibitionData && draft.exhibitionData.startDate && draft.exhibitionData.endDate) {
- console.log('draft.exhibitionData에서 날짜 데이터 복원:', {
- startDate: draft.exhibitionData.startDate,
- endDate: draft.exhibitionData.endDate,
- totalDays: draft.exhibitionData.totalDays
- });
-
newData = {
...newData,
startDate: draft.exhibitionData.startDate,
@@ -214,10 +161,11 @@ export default function ExhibitionUploadPage() {
if (draft.thumbnail) setThumbnail(draft.thumbnail);
if (draft.artworks) setArtworks(draft.artworks);
if (draft.offlineLocation) {
- console.log('오프라인 장소 데이터 복원:', draft.offlineLocation);
setOfflineLocation(draft.offlineLocation);
}
- if (draft.participants) setParticipants(draft.participants);
+ if (draft.participants && Array.isArray(draft.participants)) {
+ setParticipants(draft.participants);
+ }
if (typeof draft.contactRegistered === 'boolean') {
setContactInfo(prev => ({ ...prev, isRegistered: draft.contactRegistered }));
}
@@ -276,7 +224,26 @@ export default function ExhibitionUploadPage() {
// 연락 정보 등록 페이지에서 돌아왔을 때 전시 정보 복원
useEffect(() => {
- if (location.state?.contactUpdated && location.state?.exhibitionData) {
+ if (location.state?.contactUpdated && location.state?.draft) {
+ const { draft } = location.state;
+
+ // draft에서 모든 상태 복원
+ if (draft.exhibitionData) {
+ setExhibitionData(prev => ({
+ ...prev,
+ ...draft.exhibitionData
+ }));
+ }
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ if (draft.offlineLocation) setOfflineLocation(draft.offlineLocation);
+ if (draft.participants) setParticipants(draft.participants);
+
+ setContactInfo(prev => ({
+ ...prev,
+ isRegistered: true
+ }));
+ } else if (location.state?.contactUpdated && location.state?.exhibitionData) {
const { exhibitionData: savedExhibitionData, thumbnail: savedThumbnail, artworks: savedArtworks } = location.state;
console.log('연락 정보 등록 후 복원 데이터:', {
@@ -322,10 +289,29 @@ export default function ExhibitionUploadPage() {
// 작품 라이브러리에서 돌아왔을 때 선택된 작품들 처리
useEffect(() => {
if (location.state?.returnFromLibrary && location.state?.selectedArtworks) {
- const { selectedArtworks, artworkIndex, isThumbnail } = location.state;
+ const { selectedArtworks, artworkIndex, isThumbnail, draft } = location.state;
console.log('라이브러리에서 선택된 작품들:', selectedArtworks);
+ console.log('복원할 draft:', draft);
+
+ // 먼저 draft에서 상태 복원
+ if (draft) {
+ if (draft.exhibitionData) {
+ setExhibitionData(prev => ({
+ ...prev,
+ ...draft.exhibitionData
+ }));
+ }
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ if (draft.offlineLocation) setOfflineLocation(draft.offlineLocation);
+ if (draft.participants) setParticipants(draft.participants);
+ if (typeof draft.contactRegistered === 'boolean') {
+ setContactInfo(prev => ({ ...prev, isRegistered: draft.contactRegistered }));
+ }
+ }
+ // 그 다음 선택된 작품 처리
if (isThumbnail) {
// 썸네일로 선택된 경우
if (selectedArtworks.length > 0) {
@@ -357,6 +343,64 @@ export default function ExhibitionUploadPage() {
}
}, [location.state]);
+ // 새 작품 등록 페이지에서 돌아왔을 때 처리
+ useEffect(() => {
+ if (location.state?.returnFromArtworkUpload && location.state?.draft) {
+ const { draft, newArtwork, isThumbnail } = location.state;
+
+ console.log('새 작품 등록 후 복원 데이터:', { draft, newArtwork, isThumbnail });
+
+ // draft에서 상태 복원
+ if (draft) {
+ if (draft.exhibitionData) {
+ setExhibitionData(prev => ({
+ ...prev,
+ ...draft.exhibitionData
+ }));
+ }
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ if (draft.offlineLocation) setOfflineLocation(draft.offlineLocation);
+ if (draft.participants) setParticipants(draft.participants);
+ if (typeof draft.contactRegistered === 'boolean') {
+ setContactInfo(prev => ({ ...prev, isRegistered: draft.contactRegistered }));
+ }
+ }
+
+ // 새로 등록된 작품 처리
+ if (newArtwork) {
+ if (isThumbnail) {
+ setThumbnail(newArtwork);
+ } else {
+ addArtwork(newArtwork);
+ }
+ }
+ }
+ }, [location.state]);
+
+ // Draft 초기화 함수
+ const resetDraft = () => {
+ setExhibitionData({
+ title: '',
+ description: '',
+ startDate: '', // YYYY-MM-DD 형식 문자열로 초기화
+ endDate: '', // YYYY-MM-DD 형식 문자열로 초기화
+ totalDays: 0
+ });
+ setThumbnail(null);
+ setArtworks([]);
+ setOfflineLocation({
+ address: '',
+ addressName: '',
+ offlineDescription: ''
+ });
+ setParticipants([]);
+ setContactInfo({
+ isRegistered: false
+ });
+
+ };
+
// 전시 등록 처리
const handleSubmitExhibition = async () => {
// 필수 입력 검증
@@ -383,13 +427,20 @@ export default function ExhibitionUploadPage() {
setIsSubmitting(true);
try {
- // 실제 pieceIdList 사용
- const pieceIdList = artworks.map(artwork => artwork.pieceId);
- const participantIdList = participants.map(p => p.userId);
+ // buildDraft()에서 pieceIds 사용
+ const draft = buildDraft();
+ const pieceIdList = draft.pieceIds || [];
+ const participantIdList = participants.map(p => p.userId || p.id);
// draft에서 날짜 데이터 사용 (이미 YYYY-MM-DD 형식)
- const startDate = exhibitionData.startDate ? exhibitionData.startDate.toISOString().split('T')[0] : null;
- const endDate = exhibitionData.endDate ? exhibitionData.endDate.toISOString().split('T')[0] : null;
+ const startDate = exhibitionData.startDate;
+ const endDate = exhibitionData.endDate;
+
+ // thumbnailImageUrl 설정 (썸네일 작품의 이미지 URL)
+ let thumbnailImageUrl = null;
+ if (thumbnail) {
+ thumbnailImageUrl = thumbnail.imageUrl || thumbnail.url || thumbnail;
+ }
const exhibitionPayload = {
pieceIdList,
@@ -400,12 +451,14 @@ export default function ExhibitionUploadPage() {
title: exhibitionData.title.trim(),
offlineDescription: offlineLocation.offlineDescription || '오프라인 전시 설명 미입력',
description: exhibitionData.description.trim(),
- addressName: offlineLocation.addressName || '장소명 미입력'
+ addressName: offlineLocation.addressName || '장소명 미입력',
+ thumbnailImageUrl // 썸네일 이미지 URL 추가
};
- console.log('전시 등록 요청 데이터:', exhibitionPayload);
const response = await createExhibition(exhibitionPayload);
- console.log('전시 등록 성공:', response);
+
+ // Draft 초기화
+ resetDraft();
alert('전시가 성공적으로 등록되었습니다!');
navigate('/museum');
@@ -461,17 +514,9 @@ export default function ExhibitionUploadPage() {
}
};
- // 작품 라이브러리에서 가져오기 처리
- const handleLoadFromLibrary = () => {
- navigate('/artwork/library', {
- state: {
- fromExhibition: true,
- currentArtworkIndex,
- isChangeMode,
- isThumbnail: currentArtworkIndex === -1
- }
- });
- };
+
+
+
// 작품 추가/수정/삭제 (객체 기반)
const addArtwork = (artwork) => setArtworks(prev => [...prev, artwork]);
@@ -583,7 +628,7 @@ export default function ExhibitionUploadPage() {
{thumbnail ? (
@@ -655,15 +700,7 @@ export default function ExhibitionUploadPage() {
className={`${styles.featureButton} ${contactInfo.isRegistered ? styles.contactRegistered : ''}`}
onClick={() => navigate('/user/contact', {
state: {
- exhibitionData: {
- title: exhibitionData.title,
- description: exhibitionData.description,
- startDate: exhibitionData.startDate,
- endDate: exhibitionData.endDate,
- totalDays: exhibitionData.totalDays
- },
- thumbnail,
- artworks
+ draft: buildDraft()
}
})}
>
@@ -675,17 +712,7 @@ export default function ExhibitionUploadPage() {
className={`${styles.featureButton} ${isOfflineLocationSet() ? styles.completed : ''}`}
onClick={() => navigate('/exhibition/offline-location', {
state: {
- offlineLocation,
- returnTo: 'exhibition-upload',
- exhibitionData: {
- title: exhibitionData.title,
- description: exhibitionData.description,
- startDate: exhibitionData.startDate,
- endDate: exhibitionData.endDate,
- totalDays: exhibitionData.totalDays
- },
- thumbnail,
- artworks
+ draft: buildDraft()
}
})}
>
@@ -697,17 +724,7 @@ export default function ExhibitionUploadPage() {
className={`${styles.featureButton} ${participants.length > 0 ? styles.completed : ''}`}
onClick={() => navigate('/exhibition/participants', {
state: {
- participants,
- returnTo: 'exhibition-upload',
- exhibitionData: {
- title: exhibitionData.title,
- description: exhibitionData.description,
- startDate: exhibitionData.startDate,
- endDate: exhibitionData.endDate,
- totalDays: exhibitionData.totalDays
- },
- thumbnail,
- artworks
+ draft: buildDraft()
}
})}
>
@@ -732,10 +749,10 @@ export default function ExhibitionUploadPage() {
setIsArtworkModalOpen(false)}
- onNewArtwork={handleNewArtwork}
- onLoadFromLibrary={handleLoadFromLibrary}
isThumbnail={currentArtworkIndex === -1}
isChangeMode={isChangeMode}
+ currentDraft={buildDraft()} // 현재 상태를 모달에 전달
+ returnTo="exhibition-upload" // 돌아올 페이지 지정
/>
);
diff --git a/src/components/museum/pages/OfflineLocationPage.jsx b/src/components/museum/pages/OfflineLocationPage.jsx
index d5ccae1..be9fbbd 100644
--- a/src/components/museum/pages/OfflineLocationPage.jsx
+++ b/src/components/museum/pages/OfflineLocationPage.jsx
@@ -2,88 +2,106 @@ import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import chevronLeft from '@/assets/museum/chevron-left.png';
import styles from './offlineLocationPage.module.css';
-import SearchBar from '@/components/feed/SearchBar';
export default function OfflineLocationPage() {
const navigate = useNavigate();
const location = useLocation();
const [address, setAddress] = useState('');
- const [exhibitionDescription, setExhibitionDescription] = useState('');
+ const [addressName, setAddressName] = useState('');
+
+ // URL state에서 draft 정보 받아오기
+ const draft = location.state?.draft || {};
+ const exhibitionData = draft.exhibitionData || {};
+
-
-
- const handleBack = () => {
- // 전시 데이터를 포함하여 뒤로 가기
- const exhibitionData = location.state?.exhibitionData || {};
-
- // Date 객체를 YYYY-MM-DD 문자열로 변환
- const safeExhibitionData = {
- ...exhibitionData,
- startDate: exhibitionData.startDate instanceof Date
- ? exhibitionData.startDate.toISOString().split('T')[0]
- : exhibitionData.startDate,
- endDate: exhibitionData.endDate instanceof Date
- ? exhibitionData.endDate.toISOString().split('T')[0]
- : exhibitionData.endDate
- };
-
- console.log('뒤로 가기 시 전달할 데이터:', {
- original: exhibitionData,
- converted: safeExhibitionData
- });
-
- navigate('/exhibition/upload', {
- state: {
- draft: {
- exhibitionData: safeExhibitionData,
- thumbnail: location.state?.thumbnail || null,
- artworks: location.state?.artworks || [],
- offlineLocation: location.state?.offlineLocation || null,
- participants: location.state?.participants || [],
- contactRegistered: location.state?.contactRegistered || false
+ const handleBack = () => {
+ // draft 데이터가 있으면 그대로 전달, 없으면 기존 방식 사용
+ if (draft && Object.keys(draft).length > 0) {
+ navigate('/exhibition/upload', {
+ state: {
+ draft: draft
+ }
+ });
+ } else {
+ // 기존 방식 (하위 호환성)
+ const exhibitionData = location.state?.exhibitionData || {};
+
+ navigate('/exhibition/upload', {
+ state: {
+ draft: {
+ exhibitionData: exhibitionData,
+ thumbnail: location.state?.thumbnail || null,
+ artworks: location.state?.artworks || [],
+ offlineLocation: location.state?.offlineLocation || null,
+ participants: location.state?.participants || [],
+ contactRegistered: location.state?.contactRegistered || false
+ }
}
- }
- });
+ });
+ }
};
const handleComplete = () => {
- // 오프라인 장소 등록 완료
- console.log('오프라인 장소 등록 완료:', {
- address
- });
-
- // 전시 등록 페이지로 돌아가면서 오프라인 장소 데이터 전달
- const exhibitionData = location.state?.exhibitionData || {};
-
- navigate('/exhibition/upload', {
- state: {
- draft: {
- exhibitionData,
- thumbnail: location.state?.thumbnail || null,
- artworks: location.state?.artworks || [],
- offlineLocation: {
- address,
- addressName: address,
- offlineDescription: address
- },
- participants: location.state?.participants || [],
- contactRegistered: location.state?.contactRegistered || false
+ // draft 데이터가 있으면 기존 데이터에 오프라인 장소만 추가, 없으면 새로 생성
+ if (draft && Object.keys(draft).length > 0) {
+ const updatedDraft = {
+ ...draft,
+ offlineLocation: {
+ address,
+ addressName,
+ offlineDescription: address // 주소를 설명으로도 사용
}
- }
- });
+ };
+
+ navigate('/exhibition/upload', {
+ state: {
+ draft: updatedDraft
+ }
+ });
+ } else {
+ // 기존 방식 (하위 호환성)
+ const exhibitionData = location.state?.exhibitionData || {};
+
+ navigate('/exhibition/upload', {
+ state: {
+ draft: {
+ exhibitionData,
+ thumbnail: location.state?.thumbnail || null,
+ artworks: location.state?.artworks || [],
+ offlineLocation: {
+ address,
+ addressName,
+ },
+ participants: location.state?.participants || [],
+ contactRegistered: location.state?.contactRegistered || false
+ }
+ }
+ });
+ }
};
const renderAddressInput = () => (
);
@@ -112,7 +130,7 @@ export default function OfflineLocationPage() {
등록 완료하기
diff --git a/src/components/museum/pages/SharedLibraryEntryPage.jsx b/src/components/museum/pages/SharedLibraryEntryPage.jsx
index 222d067..6d0c59b 100644
--- a/src/components/museum/pages/SharedLibraryEntryPage.jsx
+++ b/src/components/museum/pages/SharedLibraryEntryPage.jsx
@@ -27,6 +27,10 @@ export default function SharedLibraryEntryPage() {
const [isArtworkModalOpen, setIsArtworkModalOpen] = useState(false);
const [currentArtworkIndex, setCurrentArtworkIndex] = useState(0);
const [isChangeMode, setIsChangeMode] = useState(false);
+
+ // 썸네일 및 작품 상태
+ const [thumbnail, setThumbnail] = useState(null);
+ const [artworks, setArtworks] = useState([]); // 작품 객체 배열
const handleBack = () => {
navigate('/exhibition/shared-library-selection');
@@ -40,6 +44,105 @@ export default function SharedLibraryEntryPage() {
}));
};
+ // 작품 라이브러리에서 돌아왔을 때 선택된 작품들 처리
+ useEffect(() => {
+ if (location.state?.returnFromLibrary && location.state?.selectedArtworks) {
+ const { selectedArtworks, artworkIndex, isThumbnail, draft } = location.state;
+
+ console.log('라이브러리에서 선택된 작품들:', selectedArtworks);
+ console.log('복원할 draft:', draft);
+
+ // 먼저 draft에서 상태 복원
+ if (draft) {
+ if (draft.sharedLibraryData) {
+ setSharedLibraryData(prev => ({
+ ...prev,
+ ...draft.sharedLibraryData
+ }));
+ }
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ }
+
+ // 그 다음 선택된 작품 처리
+ if (isThumbnail) {
+ // 썸네일로 선택된 경우
+ if (selectedArtworks.length > 0) {
+ setThumbnail(selectedArtworks[0]);
+ }
+ } else {
+ // 공유 라이브러리 작품으로 선택된 경우
+ if (artworkIndex !== undefined) {
+ // 특정 인덱스에 작품 추가/교체
+ if (artworkIndex >= 0) {
+ if (artworkIndex < artworks.length) {
+ // 기존 작품 교체
+ updateArtwork(artworkIndex, selectedArtworks[0]);
+ } else {
+ // 새 작품 추가
+ addArtwork(selectedArtworks[0]);
+ }
+ } else {
+ // 새 작품 추가
+ addArtwork(selectedArtworks[0]);
+ }
+ } else {
+ // 여러 작품 추가
+ selectedArtworks.forEach(artwork => {
+ addArtwork(artwork);
+ });
+ }
+ }
+ }
+ }, [location.state]);
+
+ // draft에서 상태 복원
+ useEffect(() => {
+ if (location.state?.draft) {
+ const draft = location.state.draft;
+
+ if (draft.sharedLibraryData) {
+ setSharedLibraryData(prev => ({
+ ...prev,
+ ...draft.sharedLibraryData
+ }));
+ }
+
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ }
+ }, [location.state]);
+
+ // 새 작품 등록 페이지에서 돌아왔을 때 처리
+ useEffect(() => {
+ if (location.state?.returnFromArtworkUpload && location.state?.draft) {
+ const { draft, newArtwork, isThumbnail } = location.state;
+
+ console.log('새 작품 등록 후 복원 데이터:', { draft, newArtwork, isThumbnail });
+
+ // draft에서 상태 복원
+ if (draft) {
+ if (draft.sharedLibraryData) {
+ setSharedLibraryData(prev => ({
+ ...prev,
+ ...draft.sharedLibraryData
+ }));
+ }
+ if (draft.thumbnail) setThumbnail(draft.thumbnail);
+ if (draft.artworks) setArtworks(draft.artworks);
+ }
+
+ // 새로 등록된 작품 처리
+ if (newArtwork) {
+ if (isThumbnail) {
+ setThumbnail(newArtwork);
+ } else {
+ addArtwork(newArtwork);
+ }
+ }
+ }
+ }, [location.state]);
+
const handleThumbnailChange = (e) => {
const file = e.target.files[0];
if (file) {
@@ -78,6 +181,34 @@ export default function SharedLibraryEntryPage() {
}
};
+ // 새 작품 등록 페이지로 이동
+ const handleNewArtworkPage = () => {
+ navigate('/artwork/upload', {
+ state: {
+ fromSharedLibrary: true,
+ currentArtworkIndex: currentArtworkIndex,
+ isChangeMode,
+ returnTo: 'shared-library-entry',
+ draft: buildDraft() // 현재 상태를 draft로 전달
+ }
+ });
+ };
+
+ // 현재 입력 상태를 하나로 묶어 라우팅 state로 넘길 draft
+ const buildDraft = () => {
+ const draft = {
+ sharedLibraryData,
+ thumbnail,
+ artworks,
+ startDate: sharedLibraryData.startDate || null,
+ endDate: sharedLibraryData.endDate || null,
+ totalDays: sharedLibraryData.totalDays
+ };
+
+ console.log('buildDraft 호출됨:', draft);
+ return draft;
+ };
+
// 작품 라이브러리에서 가져오기 처리
const handleLoadFromLibrary = () => {
// 작품 라이브러리 페이지로 이동
@@ -85,14 +216,25 @@ export default function SharedLibraryEntryPage() {
state: {
fromSharedLibrary: true,
currentArtworkIndex,
- isChangeMode
+ isChangeMode,
+ draft: buildDraft() // 현재 상태를 draft로 전달
}
});
};
+ // 작품 추가/수정/삭제
+ const addArtwork = (artwork) => setArtworks(prev => [...prev, artwork]);
+ const updateArtwork = (index, artwork) => {
+ setArtworks(prev => {
+ const next = [...prev];
+ next[index] = artwork;
+ return next;
+ });
+ };
+
// 작품 제거 처리
const removeArtwork = (index) => {
- removeArtworkFromStore(index);
+ setArtworks(prev => prev.filter((_, i) => i !== index));
};
// 공유 라이브러리 작품 슬라이드 렌더링
@@ -107,7 +249,7 @@ export default function SharedLibraryEntryPage() {
{artwork ? (
@@ -189,7 +331,7 @@ export default function SharedLibraryEntryPage() {
{thumbnail ? (
@@ -261,10 +403,12 @@ export default function SharedLibraryEntryPage() {
setIsArtworkModalOpen(false)}
- onNewArtwork={handleNewArtwork}
+ onNewArtwork={handleNewArtworkPage}
onLoadFromLibrary={handleLoadFromLibrary}
isThumbnail={currentArtworkIndex === -1}
isChangeMode={isChangeMode}
+ currentDraft={buildDraft()} // 현재 상태를 모달에 전달
+ returnTo="shared-library-entry" // 돌아올 페이지 지정
/>
);
diff --git a/src/components/museum/pages/offlineLocationPage.module.css b/src/components/museum/pages/offlineLocationPage.module.css
index 5c87cab..924f0ee 100644
--- a/src/components/museum/pages/offlineLocationPage.module.css
+++ b/src/components/museum/pages/offlineLocationPage.module.css
@@ -300,4 +300,17 @@
background: var(--color-main);
border: 1px solid #000;
padding: 12px 20px;
-}
\ No newline at end of file
+}
+
+.addressInput {
+ margin-bottom: 25px;
+}
+
+.inputLabel {
+ font-family: 'Pretendard', sans-serif;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 25px;
+ letter-spacing: -0.4px;
+ color: #000;
+}
diff --git a/src/components/user/ContactEditPage.jsx b/src/components/user/ContactEditPage.jsx
index a07b47f..7ee706f 100644
--- a/src/components/user/ContactEditPage.jsx
+++ b/src/components/user/ContactEditPage.jsx
@@ -12,6 +12,7 @@ export default function ContactEditPage() {
// 전시 정보 상태 (전시 업로드 페이지에서 넘어온 경우)
const [exhibitionData, setExhibitionData] = useState(null);
+ const [draft, setDraft] = useState(null);
// 폼 상태 관리
const [formData, setFormData] = useState({
@@ -26,7 +27,11 @@ export default function ContactEditPage() {
const fetchData = async () => {
try {
// 전시 정보가 있는지 확인 (전시 업로드 페이지에서 넘어온 경우)
- if (location.state?.exhibitionData) {
+ if (location.state?.draft) {
+ console.log('ContactEditPage에서 받은 draft 데이터:', location.state.draft);
+ setDraft(location.state.draft);
+ setExhibitionData(location.state.draft.exhibitionData);
+ } else if (location.state?.exhibitionData) {
console.log('ContactEditPage에서 받은 전시 데이터:', location.state);
setExhibitionData(location.state.exhibitionData);
}
@@ -39,7 +44,6 @@ export default function ContactEditPage() {
// 연락 정보 조회
const contactResponse = await getUserContact(userId);
- console.log('연락 정보 API 응답:', contactResponse);
if (contactResponse && contactResponse.data) {
const contactData = contactResponse.data;
@@ -79,21 +83,21 @@ export default function ContactEditPage() {
? `${formData.emailUsername}@${formData.customDomain}`
: `${formData.emailUsername}@${formData.emailDomain}`;
- console.log('저장할 연락정보:', {
- email: finalEmail,
- instagram: formData.instagram
- });
-
// API로 연락 정보 전송
const response = await updateContact({
email: finalEmail,
instagram: formData.instagram
});
- console.log('연락 정보 등록 성공:', response);
-
// 편집 완료 후 전시 정보가 있으면 전시 업로드 페이지로, 없으면 프로필 페이지로 이동
- if (location.state?.exhibitionData) {
+ if (draft) {
+ navigate('/exhibition/upload', {
+ state: {
+ draft: draft,
+ contactUpdated: true
+ }
+ });
+ } else if (location.state?.exhibitionData) {
navigate('/exhibition/upload', {
state: {
exhibitionData: location.state.exhibitionData,
@@ -113,7 +117,14 @@ export default function ContactEditPage() {
const handleBackClick = () => {
// 전시 등록 페이지에서 이동한 경우 전시 업로드 페이지로, 아니면 프로필 페이지로 이동
- if (location.state?.exhibitionData) {
+ if (draft) {
+ navigate('/exhibition/upload', {
+ state: {
+ draft: draft,
+ contactUpdated: false // 뒤로가기이므로 contactUpdated는 false
+ }
+ });
+ } else if (location.state?.exhibitionData) {
navigate('/exhibition/upload', {
state: {
exhibitionData: location.state.exhibitionData,
diff --git a/vite.config.js b/vite.config.js
index 23af6a8..09bb4a1 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -18,13 +18,12 @@ export default defineConfig({
],
},
server: {
- host: '0.0.0.0',
- port: 5173,
proxy: {
+ // "/api"로 들어오는 요청을 백엔드로 프록시
'/api': {
- target: 'https://api.artium.life',
- changeOrigin: true,
- }
- }
- }
+ target: 'https://api.artium.life', // 백엔드 서버
+ changeOrigin: true, // 호스트 헤더를 target으로 변경
+ },
+ },
+ },
});
\ No newline at end of file
From d2a95cff04d1766e0111f51935d9e14ff6fc083f Mon Sep 17 00:00:00 2001
From: wnsgur393 <2021301022@skuniv.ac.kr>
Date: Mon, 25 Aug 2025 03:38:13 +0900
Subject: [PATCH 6/6] =?UTF-8?q?merge=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?=
=?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/museum/{exhibition.js => museum-exhibition.js} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/apis/museum/{exhibition.js => museum-exhibition.js} (100%)
diff --git a/src/apis/museum/exhibition.js b/src/apis/museum/museum-exhibition.js
similarity index 100%
rename from src/apis/museum/exhibition.js
rename to src/apis/museum/museum-exhibition.js