diff --git a/src/apis/cards.js b/src/apis/cards.js index 64aa786..93fce5b 100644 --- a/src/apis/cards.js +++ b/src/apis/cards.js @@ -7,6 +7,13 @@ export const getCards = async () => { return response; }; +// 단건 명함 조회 +export const getCardDetail = async ({ card_id }) => { + const response = await authAxios.get(`/cards/${card_id}`); + console.log(response.data.result); + return response; +}; + // 명함 생성 export const postCards = async ({ data }) => { const response = await authAxios.post(`/cards`, data); diff --git a/src/apis/index.js b/src/apis/index.js index 4b48e61..827152d 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -1 +1,3 @@ export { getGroupList, postGroup, deleteGroup, putGroup } from './group'; +export { getCards, getCardDetail, postCards, deleteCards, putCards, searchCards } from './cards'; +export { getMyCard, postMyCard, deleteMyCard, putMyCard } from './myCard'; diff --git a/src/axios/index.js b/src/axios/index.js index f675705..b19abb7 100644 --- a/src/axios/index.js +++ b/src/axios/index.js @@ -6,7 +6,7 @@ export const authAxios = axios.create({ baseURL: VITE_SERVER_DOMAIN, withCredentials: true, headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'multipart/form-data', }, }); diff --git a/src/components/CardInfo/CardInfo.jsx b/src/components/CardInfo/CardInfo.jsx index 4b1debb..c566fb9 100644 --- a/src/components/CardInfo/CardInfo.jsx +++ b/src/components/CardInfo/CardInfo.jsx @@ -5,9 +5,10 @@ import Icon from '../../components/Icon/Icon'; import { Link } from 'react-router-dom'; export default function CardInfo({ + id, name, - role, - company, + position, + department, imageUrl, isDeleteMode = false, isSelected = false, @@ -30,7 +31,7 @@ export default function CardInfo({ {name} - {role} / {company} + {position} / {department} @@ -50,14 +51,15 @@ export default function CardInfo({ return isDeleteMode ? ( cardContent ) : ( - {cardContent} + {cardContent} ); } CardInfo.propTypes = { name: PropTypes.string.isRequired, - role: PropTypes.string.isRequired, company: PropTypes.string.isRequired, + position: PropTypes.string.isRequired, + department: PropTypes.string.isRequired, imageUrl: PropTypes.string, isDeleteMode: PropTypes.bool, isSelected: PropTypes.bool, diff --git a/src/hooks/useFormData.js b/src/hooks/useFormData.js new file mode 100644 index 0000000..d7cbef7 --- /dev/null +++ b/src/hooks/useFormData.js @@ -0,0 +1,18 @@ +import { useCallback } from 'react'; + +function useFormData(data) { + return useCallback(() => { + const formData = new FormData(); + + // 객체의 모든 키-값 쌍을 FormData에 추가 + for (let key in data) { + if (data.hasOwnProperty(key) && data[key]) { + formData.append(key, data[key]); + } + } + + return formData; + }, [data]); +} + +export default useFormData; diff --git a/src/pages/CardDetailPage/CardDetailPage.jsx b/src/pages/CardDetailPage/CardDetailPage.jsx index 9697404..9a264de 100644 --- a/src/pages/CardDetailPage/CardDetailPage.jsx +++ b/src/pages/CardDetailPage/CardDetailPage.jsx @@ -1,14 +1,29 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import * as S from './CardDetailPage.style'; import Icon from '../../components/Icon/Icon'; import { DetailBadge } from '../../components'; import ProfileImgDefault from '../../assets/images/profile-img-default.svg'; import CARDS_SAMPLE_DATA from '../../constants/cardsSampleData'; +import { getCardDetail } from '../../apis/cards'; +import { useQuery } from '@tanstack/react-query'; export default function CardDetailPage() { + const [info, setInfo] = useState({}); + console.log(info); const { id } = useParams(); + const { data: inputData } = useQuery({ + queryKey: ['cardDetail', id], + queryFn: () => getCardDetail({ card_id: id }), + }); + + useEffect(() => { + if (inputData) { + setInfo(inputData.data); + } + }, [inputData]); + const badges = [ { label: '비즈니스', value: '비즈니스' }, { label: '방송사', value: '방송사' }, @@ -24,22 +39,22 @@ export default function CardDetailPage() { ? badges.filter((badge) => badge.value === filteredData.category) : []; - const data = filteredData || { - imageUrl: '', - name: '이름없음', - job: '직책없음', - team: '팀없음', - company: '회사없음', - phone: '전화번호없음', - email: '이메일없음', - tel: '유선전화없음', - address: '주소없음', - memo: '메모없음', - pic1: '사진없음', - pic2: '사진없음', - }; + // const data = filteredData || { + // imageUrl: '', + // name: '이름없음', + // job: '직책없음', + // team: '팀없음', + // company: '회사없음', + // phone: '전화번호없음', + // email: '이메일없음', + // tel: '유선전화없음', + // address: '주소없음', + // memo: '메모없음', + // pic1: '사진없음', + // pic2: '사진없음', + // }; - const profileImageUrl = data.imageUrl || ProfileImgDefault; + const profileImageUrl = info.profImgUrl || ProfileImgDefault; const activeBadge = filteredData ? filteredData.category : ''; @@ -69,16 +84,16 @@ export default function CardDetailPage() { - + - {data.name} + {info.name} - {data.job} / {data.team} + {info.position} / {info.department} - {data.company} + {info.company} @@ -90,7 +105,7 @@ export default function CardDetailPage() { 휴대폰 - {data.phone} + {info.phone} @@ -100,40 +115,40 @@ export default function CardDetailPage() { 이메일 - {data.email} + {info.email} 유선전화 - {data.tel} + {info.tel} 주소 - {data.address} + {info.address} 메모 - {data.memo} + {info.memo} 그룹 - {(data.pic1 || data.pic2) && ( + {(info.pic1 || info.pic2) && ( - handleImageClick(data.pic1)}> - 사진 1 + handleImageClick(info.pic1)}> + 사진 1 - {data.pic2 ? ( - handleImageClick(data.pic2)}> - 사진 2 + {info.pic2 ? ( + handleImageClick(info.pic2)}> + 사진 2 ) : ( diff --git a/src/pages/DetailEditPage/DetailEditPage.jsx b/src/pages/DetailEditPage/DetailEditPage.jsx index 058ba8b..78899a8 100644 --- a/src/pages/DetailEditPage/DetailEditPage.jsx +++ b/src/pages/DetailEditPage/DetailEditPage.jsx @@ -3,10 +3,10 @@ import { useParams, Link, useNavigate } from 'react-router-dom'; import * as S from './DetailEditPage.style'; import Icon from '../../components/Icon/Icon'; import { BlueBadge, AddGroupModal } from '../../components'; -import CARDS_SAMPLE_DATA from '../../constants/cardsSampleData'; import ProfileImgDefault from '../../assets/images/profile-img-default.svg'; -import { getGroupList } from '../../apis'; +import { getGroupList, getCardDetail, putCards } from '../../apis'; import { useQuery } from '@tanstack/react-query'; +import useFormData from '../../hooks/useFormData'; const InputWrapper = memo( ({ @@ -47,241 +47,155 @@ const InputWrapper = memo( export default function DetailEditPage() { const { id } = useParams(); - const [activeBadge, setActiveBadge] = useState(null); - - const { - data: groupListData, - isLoading, - isError, - } = useQuery({ - queryKey: ['groupList'], - queryFn: () => getGroupList(), - // enabled: !!member_id, - }); + const navigate = useNavigate(); + const [activeBadge, setActiveBadge] = useState(null); const [badges, setBadges] = useState([]); - - useEffect(() => { - if (groupListData) { - console.log('groupListData: ', groupListData.data); - const initialBadges = groupListData.data.map((group) => ({ - label: group.id, // or whatever label makes sense - value: group.name, - })); - setBadges(initialBadges); // Set badges to the fetched groups - } - }, [groupListData]); - - const filteredData = CARDS_SAMPLE_DATA.find( - (data) => data.name === decodeURIComponent(id) - ); - - // const [filteredBadges, setFilteredBadges] = useState(() => { - // return filteredData ? groupListData : []; - // }); - - const data = filteredData || { - imageUrl: '', - }; - const [profileImage, setProfileImage] = useState(null); - + const [modalVisible, setModalVisible] = useState(false); const [selectedImage, setSelectedImage] = useState({ pic1: null, pic2: null, }); + const [info, setInfo] = useState({}); const profileImageInputRef = useRef(null); const profileImageInputRef1 = useRef(null); const profileImageInputRef2 = useRef(null); - const onUploadImage = (e) => { - const files = Array.from(e.target.files || e.dataTransfer.files); - setSelectedImage(files); - }; - - const onUploadProfileImage = (e) => { - const file = e.target.files[0]; - if (file) { - setProfileImage(URL.createObjectURL(file)); - } - }; - - const handleProfileImageClick = () => { - profileImageInputRef.current.click(); - }; - - const handleFirstImageClick = () => { - profileImageInputRef1.current.click(); - }; - - const handleSecondImageClick = () => { - profileImageInputRef2.current.click(); - }; - - const handleImageUpload = (e, target) => { - const file = e.target.files[0]; - if (file) { - const newImageUrl = URL.createObjectURL(file); // 업로드된 파일의 URL 생성 - - setSelectedImage((prev) => ({ - ...prev, - [target]: newImageUrl, // target(pic1, pic2)에 따라 상태 업데이트 - })); - } - }; - - const [myInfo, setMyInfo] = useState({ - name: filteredData.name, - job: filteredData.job, - company: filteredData.company, - team: filteredData.team, - }); - - const [myContact, setMyContact] = useState({ - phone: filteredData.phone, - email: filteredData.email, - tel: filteredData.tel, - address: filteredData.address, - memo: filteredData.memo, - }); - const handleInfoChange = (e) => { const { name, value } = e.target; - setMyInfo((prevInfo) => ({ + setInfo((prevInfo) => ({ ...prevInfo, [name]: value, })); }; - const handleContactChange = (e) => { - const { name, value } = e.target; - setMyContact((prevContact) => ({ - ...prevContact, - [name]: value, - })); - }; - + // 입력할 데이터 const InputData = [ { label: '회사명', type: 'text', name: 'company', - value: myInfo.company, + value: info.company, onChange: handleInfoChange, }, { label: '직책', type: 'text', - name: 'job', - value: myInfo.job, + name: 'position', + value: info.position, onChange: handleInfoChange, }, { label: '부서', type: 'text', - name: 'team', - value: myInfo.team, + name: 'department', + value: info.department, onChange: handleInfoChange, }, { label: '휴대폰', type: 'tel', name: 'phone', - value: myContact.phone, - onChange: handleContactChange, + value: info.phone, + onChange: handleInfoChange, }, { label: '이메일', type: 'email', name: 'email', - value: myContact.email, - onChange: handleContactChange, + value: info.email, + onChange: handleInfoChange, }, { label: '유선전화', type: 'tel', name: 'tel', - value: myContact.tel, - onChange: handleContactChange, + value: info.tel, + onChange: handleInfoChange, }, { label: '주소', type: 'text', name: 'address', - value: myContact.address, - onChange: handleContactChange, + value: info.address, + onChange: handleInfoChange, }, { label: '메모', type: 'text', name: 'memo', - value: myContact.memo, - onChange: handleContactChange, + value: info.memo, + onChange: handleInfoChange, }, ]; - const handleEditClick = (field) => { - setIsEditing((prev) => ({ - ...prev, - [field]: true, - })); - }; - - const handleBlur = (field) => { - setIsEditing((prev) => ({ - ...prev, - [field]: false, - })); - }; + // 카드 상세 데이터 + const { data: inputData } = useQuery({ + queryKey: ['cardDetail', id], + queryFn: () => getCardDetail({ card_id: id }), + }); - const handleFocus = (field) => { - setIsEditing((prev) => ({ - ...prev, - [field]: true, - })); - }; + const profileImageUrl = profileImage || ProfileImgDefault; - const [isEditing, setIsEditing] = useState({ - name: false, - job: false, - company: false, - phone: false, - email: false, - tel: false, - address: false, + const { data: groupListData } = useQuery({ + queryKey: ['groupList'], + queryFn: () => getGroupList(), }); - const navigate = useNavigate(); + useEffect(() => { + if (groupListData) { + const initialBadges = groupListData.data.map((group) => ({ + label: group.id, + value: group.name, + })); + setBadges(initialBadges); + } + }, [groupListData]); - const handleEditComplete = () => { - const updatedData = { - ...myInfo, - ...myContact, - group: activeBadge, - }; + useEffect(() => { + if (inputData) { + setInfo(inputData.data); // inputData로부터 info 상태 업데이트 + } + }, [inputData]); - console.log('Data saved successfully:', updatedData); - setIsEditing({ - name: false, - job: false, - company: false, - phone: false, - email: false, - tel: false, - address: false, - }); - navigate(`/card/${id}`); + const onUploadProfileImage = (e) => { + const file = e.target.files[0]; + if (file) { + setProfileImage(URL.createObjectURL(file)); + } }; - const profileImageUrl = profileImage || data.imageUrl || ProfileImgDefault; - - const [modalVisible, setModalVisible] = useState(false); + const handleProfileImageClick = () => { + profileImageInputRef.current.click(); + }; - const openModal = () => { - setModalVisible(true); + const handleImageUpload = (e, target) => { + const file = e.target.files[0]; + if (file) { + const newImageUrl = URL.createObjectURL(file); // 업로드된 파일의 URL 생성 + setSelectedImage((prev) => ({ + ...prev, + [target]: newImageUrl, // target(pic1, pic2)에 따라 상태 업데이트 + })); + } }; + // useFormData 훅을 컴포넌트 내에서 호출 + const updatedDataForm = useFormData({ + ...info, + group: activeBadge, + }); + + const handleEditComplete = async () => { + try { + await putCards({ card_id: id, data: updatedDataForm() }); // updatedDataForm() 호출 + navigate(`/card/${id}`); // 편집 완료 후 해당 카드 페이지로 리디렉션 + } catch (error) { + console.error('데이터를 저장하는 중에 오류가 발생하였습니다.:', error); + } + }; return ( <> @@ -326,30 +240,14 @@ export default function DetailEditPage() { - {isEditing.name ? ( - handleBlur('name')} - onFocus={() => handleFocus('name')} - autoFocus - /> - ) : ( - <> - {myInfo.name} - handleEditClick('name')}> - - - - )} + {info.name} 사진 아이콘을 클릭하여 명함에 들어갈 프로필 사진을 수정하세요 + {InputData.map((field, index) => ( @@ -358,16 +256,13 @@ export default function DetailEditPage() { label={field.label} type={field.type} name={field.name} - value={isEditing[field.name] ? field.value : ''} - placeholder={isEditing[field.name] ? '' : field.value} + value={field.value} onChange={field.onChange} - onBlur={() => handleBlur(field.name)} - onFocus={() => handleFocus(field.name)} - autoFocus={isEditing[field.name]} /> ))} + 그룹 @@ -376,63 +271,53 @@ export default function DetailEditPage() { activeBadge={activeBadge} setActiveBadge={setActiveBadge} /> - - 그룹 수정 - - - - - {(data.pic1 || data.pic2) && ( - - - 사진 1 - - - handleImageUpload(e, 'pic1')} - /> - - - {data.pic2 ? ( - - 사진 2 + {/* 이미지 업로드 */} + + + 사진 1 + + profileImageInputRef1.current.click()} + /> + handleImageUpload(e, 'pic1')} + /> + + - - - handleImageUpload(e, 'pic2')} - /> - - - ) : ( - - )} - - )} + + 사진 2 + + profileImageInputRef2.current.click()} + /> + handleImageUpload(e, 'pic2')} + /> + + + + ( ))} diff --git a/src/pages/ViewCardPage/ViewCardPage.jsx b/src/pages/ViewCardPage/ViewCardPage.jsx index b0b1f9e..fb826a2 100644 --- a/src/pages/ViewCardPage/ViewCardPage.jsx +++ b/src/pages/ViewCardPage/ViewCardPage.jsx @@ -1,14 +1,15 @@ import * as S from './ViewCardPage.style'; import { Header, SearchBar, BlueBadge, CardInfo } from '../../components'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Icon from '../../components/Icon/Icon'; -import CARDS_SAMPLE_DATA from '../../constants/cardsSampleData'; +import { getCards } from '../../apis/cards'; export default function ViewCardPage() { const [activeBadge, setActiveBadge] = useState('전체 보기'); const [isEditCompleteVisible, setIsEditCompleteVisible] = useState(false); const [isDeleteMode, setIsDeleteMode] = useState(false); const [selectedCards, setSelectedCards] = useState([]); + const [cardsData, setCardsData] = useState([]); const badges = [ { label: '전체 보기', value: '전체 보기' }, { label: '비즈니스', value: '비즈니스' }, @@ -17,10 +18,23 @@ export default function ViewCardPage() { { label: '대학교', value: '대학교' }, ]; + async function fetchCards() { + try { + const response = await getCards(); + setCardsData(response.data.cards); + } catch (error) { + console.error('카드 리스트를 불러오지 못했습니다.', error); + } + } + + useEffect(() => { + fetchCards(); + }, []); + let filteredData = activeBadge === '전체 보기' - ? CARDS_SAMPLE_DATA - : CARDS_SAMPLE_DATA.filter((data) => data.category === activeBadge); + ? cardsData + : cardsData.filter((data) => data.category === activeBadge); // 이름을 기준으로 오름차순 정렬 filteredData = filteredData.sort((a, b) => a.name.localeCompare(b.name)); @@ -81,9 +95,10 @@ export default function ViewCardPage() { {filteredData.map((data, index) => (