diff --git a/package-lock.json b/package-lock.json index 9b00acf..f0f8234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react-scripts": "^5.0.1", "react-swipeable": "^7.0.1", "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "request": "^2.88.2", "source-map": "^0.7.4", "styled-components": "^6.1.11", @@ -13971,6 +13972,14 @@ } } }, + "node_modules/recoil-persist": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/recoil-persist/-/recoil-persist-5.1.0.tgz", + "integrity": "sha512-sew4k3uBVJjRWKCSFuBw07Y1p1pBOb0UxLJPxn4G2bX/9xNj+r2xlqYy/BRfyofR/ANfqBU04MIvulppU4ZC0w==", + "peerDependencies": { + "recoil": "^0.7.2" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", diff --git a/package.json b/package.json index 44a9f6e..d80f522 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react-scripts": "^5.0.1", "react-swipeable": "^7.0.1", "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "request": "^2.88.2", "source-map": "^0.7.4", "styled-components": "^6.1.11", diff --git a/src/apis/deleteShareGroup.ts b/src/apis/deleteShareGroup.ts new file mode 100644 index 0000000..e4a0b5b --- /dev/null +++ b/src/apis/deleteShareGroup.ts @@ -0,0 +1,10 @@ +import { authInstance } from './instance'; + +export async function deleteShareGroup(shareGroupId: number): Promise { + try { + await authInstance().delete(`/shareGroups/${shareGroupId}`); + } catch (error) { + console.error('Error deleting share group:', error); + throw error; + } +} diff --git a/src/apis/getMembers.ts b/src/apis/getMembers.ts index 0cddfb1..0bc03df 100644 --- a/src/apis/getMembers.ts +++ b/src/apis/getMembers.ts @@ -3,6 +3,7 @@ import { emailResponse, marketingResponse, deleteResponse, + samplePhotoResponse, } from '../recoil/types/members'; import axios from 'axios'; @@ -81,3 +82,24 @@ export const deleteUser = async (memberId: number) => { } } }; + +// 샘플 사진 업로드 여부 조회 (GET) +export const getHasSamplePhoto = async () => { + try { + const response = + await authInstance().get(`/members/samplePhoto`); + const { status, code, message, data } = response.data; + + if (status === 200) { + return data.hasSamplePhoto; + } else { + throw new Error(`Error ${code}: ${message}`); + } + } catch (err: unknown) { + if (axios.isAxiosError(err)) { + throw new Error(`Axios error: ${err.message}`); + } else { + throw new Error('샘플 사진 업로드 여부 조회 중 오류 발생.'); + } + } +}; diff --git a/src/apis/getMyInviteCode.ts b/src/apis/getMyInviteCode.ts new file mode 100644 index 0000000..487e60f --- /dev/null +++ b/src/apis/getMyInviteCode.ts @@ -0,0 +1,29 @@ +import { authInstance } from './instance'; + +interface IInviteCode { + inviteCode: string; + inviteUrl: string; +} + +export async function getMyInviteCode({ + shareGroupId, +}: { + shareGroupId: number; +}): Promise { + try { + const response = await authInstance().get( + `/shareGroups/${shareGroupId}/invite`, + ); + if (response.status === 200) { + return { + inviteCode: response.data.inviteCode, + inviteUrl: response.data.inviteUrl, + }; + } else { + throw new Error('Failed to fetch invite code'); + } + } catch (error) { + console.error('Error fetching invite code:', error); + throw error; + } +} diff --git a/src/apis/getMyShareGroup.ts b/src/apis/getMyShareGroup.ts new file mode 100644 index 0000000..9c060d6 --- /dev/null +++ b/src/apis/getMyShareGroup.ts @@ -0,0 +1,59 @@ +import { ApiResponse } from 'recoil/types/notice'; +import { authInstance } from './instance'; + +interface IShareGroupInfo { + shareGroupId: number; + name: string; + image: string; + memberCount: number; + inviteUrl: string; + createdAt: string; +} + +interface profile { + profileId: number; // 프로필 id + name: string; // 프로필 이름 + image: string; // URL 형식 + memberId: number; // 해당 프로필로 참여한 회원의 id. 생략할지 고민중 +} + +interface IShareGroupMember { + shareGroupId: number; + name: string; + image: string; + memberCount: number; + inviteUrl: string; + createdAt: string; + profileInfoList: profile[]; +} + +// eslint-disable-next-line prettier/prettier +export async function getMyShareGroup(): Promise { + try { + const response = await authInstance().get('/shareGroups/my'); + if (response.status === 200) { + return response.data.data.shareGroupInfoList; + } else { + throw new Error('Failed to fetch share group'); + } + } catch (error) { + console.error('Error fetching share group:', error); + throw error; + } +} + +export async function getShareGroupMembers( + shareGroupId: number, +): Promise { + try { + const response = await authInstance().get(`/shareGroups/${shareGroupId}`); + if (response.status === 200) { + return response.data.data; + } else { + throw new Error('Failed to fetch share group members'); + } + } catch (error) { + console.error('Error fetching share group members:', error); + throw error; + } +} diff --git a/src/apis/postPhotoUpload.ts b/src/apis/postPhotoUpload.ts index 6631d81..af20238 100644 --- a/src/apis/postPhotoUpload.ts +++ b/src/apis/postPhotoUpload.ts @@ -1,7 +1,7 @@ import { PostApiResponse } from 'recoil/types/notice'; import { authInstance } from './instance'; interface requestProp { - shareGroupId: number; + shareGroupId?: number; photoUrlList: string[]; } @@ -9,10 +9,8 @@ export const postPhotoUpload = async ( requestData: requestProp, ): Promise<{ data: any }> => { try { - const res = await authInstance().post( - `/photos/upload`, - requestData, - ); + const url = requestData.shareGroupId ? '/photos/upload' : '/photos/sample'; + const res = await authInstance().post(url, requestData); const { status, code, message, data } = res.data; if (status === 200) { return { data }; diff --git a/src/apis/postPresignedUrl.ts b/src/apis/postPresignedUrl.ts index 51abd87..3a2fede 100644 --- a/src/apis/postPresignedUrl.ts +++ b/src/apis/postPresignedUrl.ts @@ -2,7 +2,6 @@ import { PostApiResponse } from 'recoil/types/notice'; import { authInstance } from './instance'; interface requestProp { - shareGroupId: number; photoNameList: string[]; } diff --git a/src/assets/background/cloud_upper_below.png b/src/assets/background/cloud_upper_below.png index 36d6cc9..36abe19 100644 Binary files a/src/assets/background/cloud_upper_below.png and b/src/assets/background/cloud_upper_below.png differ diff --git a/src/assets/samples/emptyProfile.png b/src/assets/samples/emptyProfile.png new file mode 100644 index 0000000..5cb8340 Binary files /dev/null and b/src/assets/samples/emptyProfile.png differ diff --git a/src/components/Common/DropDown/Styles.ts b/src/components/Common/DropDown/Styles.ts index bab5d2d..8194e73 100644 --- a/src/components/Common/DropDown/Styles.ts +++ b/src/components/Common/DropDown/Styles.ts @@ -38,7 +38,7 @@ export const ExpendLayout = styled.div` position: ${({ noIndexTag }) => (noIndexTag ? 'absolute' : 'relative')}; top: 10%; left: 2rem; - z-index: 1; + z-index: 2; `; export const IconLayout = styled.div` diff --git a/src/components/Header/Styles.ts b/src/components/Header/Styles.ts index 7b684a4..dc50f53 100644 --- a/src/components/Header/Styles.ts +++ b/src/components/Header/Styles.ts @@ -11,4 +11,6 @@ export const Layout = styled.div` padding: 0 1rem; `; -export const IconLayout = styled.button``; +export const IconLayout = styled.button` + cursor: pointer; +`; diff --git a/src/components/MyPage/MyPageMain.tsx b/src/components/MyPage/MyPageMain.tsx index 8648609..3efbbba 100644 --- a/src/components/MyPage/MyPageMain.tsx +++ b/src/components/MyPage/MyPageMain.tsx @@ -1,28 +1,22 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import * as S from './Styles'; import logoblurred from '../../assets/logo/typo-blurred.png'; import background from 'assets/background/cloudLeft.png'; -import { getMyInfo } from 'apis/getMyInfo'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { myPageModalState, modalMessageState, modalDataState, } from 'recoil/states/mypage'; - -interface responseType { - name: string; - email: string; - image: string; - memberId: number; // memberId 추가 -} +import { UserState } from 'recoil/states/enter'; const MyPageMain = () => { - const [data, setData] = useState(); + const userInfo = useRecoilValue(UserState); const setModalOpen = useSetRecoilState(myPageModalState); const setModalMessage = useSetRecoilState(modalMessageState); const setModalData = useSetRecoilState(modalDataState); + const openLogoutModal = () => { setModalMessage('로그아웃 하시겠습니까?'); setModalOpen(true); @@ -30,24 +24,19 @@ const MyPageMain = () => { const openWithdrawalModal = () => { setModalMessage('탈퇴하시겠습니까? 데이터는 복구할 수 없습니다.'); - if (data) { - setModalData({ memberId: data.memberId }); + if (userInfo) { + setModalData({ memberId: userInfo.memberId }); setModalOpen(true); } else setModalOpen(true); }; - useEffect(() => { - getMyInfo() - .then((res) => setData(res.data)) - .catch((error) => console.error(error)); - }, []); return ( - + - {data?.email} - {data?.name} + {userInfo?.email} + {userInfo?.name} diff --git a/src/components/MyPage/Styles.ts b/src/components/MyPage/Styles.ts index a486b12..39d8599 100644 --- a/src/components/MyPage/Styles.ts +++ b/src/components/MyPage/Styles.ts @@ -18,7 +18,7 @@ export const ProfileContainer = styled.div` transform: translate(-50%, -50%); `; -export const Profile = styled.div<{ image?: string }>` +export const Profile = styled.img` position: absolute; width: 35%; height: 16%; @@ -26,8 +26,7 @@ export const Profile = styled.div<{ image?: string }>` top: 35%; left: 50%; transform: translate(-50%, -50%); - background-image: ${(props) => - props.image ? `url(${props.image})` : `url(${I.Profile})`}; + object-fit: cover; `; export const EmailText = styled.div` diff --git a/src/components/ShareGroup/ShareGroupAddButton/ShareGroupAddButton.tsx b/src/components/ShareGroup/ShareGroupAddButton/ShareGroupAddButton.tsx index ba0e5d7..4b7d648 100644 --- a/src/components/ShareGroup/ShareGroupAddButton/ShareGroupAddButton.tsx +++ b/src/components/ShareGroup/ShareGroupAddButton/ShareGroupAddButton.tsx @@ -26,10 +26,10 @@ const ShareGroupAddButton: React.FC = ({ {showButton && ( - + - + diff --git a/src/components/ShareGroup/ShareGroupCarousel/Styles.ts b/src/components/ShareGroup/ShareGroupCarousel/Styles.ts index a6ed676..75f0748 100644 --- a/src/components/ShareGroup/ShareGroupCarousel/Styles.ts +++ b/src/components/ShareGroup/ShareGroupCarousel/Styles.ts @@ -17,10 +17,9 @@ export const CarouselTrack = styled.div<{ `; export const CarouselBlankItem = styled.div<{ isRight?: boolean }>` - height: 200px; background: red; - margin-right: ${(props) => (props.isRight ? '4rem' : 0)}; - margin-left: ${(props) => (props.isRight ? 0 : '4rem')}; + margin-right: ${(props) => (props.isRight ? '18%' : 0)}; + margin-left: ${(props) => (props.isRight ? 0 : '18%')}; `; export const Dot = styled.div<{ active: boolean }>` diff --git a/src/components/ShareGroup/ShareGroupCarouselItem/ShareGroupCarouselItem.tsx b/src/components/ShareGroup/ShareGroupCarouselItem/ShareGroupCarouselItem.tsx index f5259e3..65e2ad6 100644 --- a/src/components/ShareGroup/ShareGroupCarouselItem/ShareGroupCarouselItem.tsx +++ b/src/components/ShareGroup/ShareGroupCarouselItem/ShareGroupCarouselItem.tsx @@ -1,9 +1,10 @@ import React from 'react'; import * as S from './Styles'; import { useNavigate, useParams } from 'react-router-dom'; +import defaultProfile from '../../../assets/samples/emptyProfile.png'; interface CarouselItemProps { - profileId: number; + profileId?: number; active: boolean; profileImage?: string; name?: string; @@ -22,7 +23,11 @@ const ShareGroupCarouselItem: React.FC = ({ active={active} onClick={() => navigatte(`/group/detail`, { - state: { shareGroupId: id, profileId: profileId }, + state: { + shareGroupId: id, + profileId: profileId, + profileImage: profileImage, + }, }) } > @@ -30,9 +35,9 @@ const ShareGroupCarouselItem: React.FC = ({ {profileImage ? ( - + ) : ( - + )} {name} diff --git a/src/components/ShareGroup/ShareGroupCloudButton/ShareGroupCloudButton.tsx b/src/components/ShareGroup/ShareGroupCloudButton/ShareGroupCloudButton.tsx index 0a1d938..6767df5 100644 --- a/src/components/ShareGroup/ShareGroupCloudButton/ShareGroupCloudButton.tsx +++ b/src/components/ShareGroup/ShareGroupCloudButton/ShareGroupCloudButton.tsx @@ -3,6 +3,7 @@ import * as S from './Styles'; import { postPresignedUrl } from 'apis/postPresignedUrl'; import axios from 'axios'; import { postPhotoUpload } from 'apis/postPhotoUpload'; +import { useParams } from 'react-router-dom'; interface responseProp { photoName: string; @@ -14,6 +15,7 @@ const ShareGroupCloudButton: React.FC = () => { const [file, setFile] = useState(null); const [response, setResponse] = useState([]); const [photoUrl, setPhotoUrl] = useState(); + const { id } = useParams<{ id: string }>(); const handleAddButtonClick = () => { const fileInput = document.getElementById('file') as HTMLInputElement; @@ -30,13 +32,15 @@ const ShareGroupCloudButton: React.FC = () => { setFile(fileList); if (nameList) { const requestData = { - shareGroupId: 2, photoNameList: nameList, }; try { const { data } = await postPresignedUrl(requestData); + const photoUrls = data.preSignedUrlInfoList.map( + (item: any) => item.photoUrl, + ); setResponse(data.preSignedUrlInfoList); - setPhotoUrl(data.photoUrl); + setPhotoUrl(photoUrls); } catch (error) { console.error('Error: ', error); } @@ -44,7 +48,7 @@ const ShareGroupCloudButton: React.FC = () => { }; const handleUpload = async () => { - if (!response || !file) return; + if (!response || !file || !id) return; try { const uploadPromises = Array.from(file).map(async (fileItem, index) => { const presignedUrl = response[index].preSignedUrl; @@ -59,7 +63,7 @@ const ShareGroupCloudButton: React.FC = () => { }); await Promise.all(uploadPromises); // 모든 업로드가 완료될 때까지 대기 const requestData = { - shareGroupId: 2, + shareGroupId: parseInt(id), photoUrlList: photoUrl || [], }; postPhotoUpload(requestData); diff --git a/src/components/ShareGroup/ShareGroupFolderView/ShareGroupFolderView.tsx b/src/components/ShareGroup/ShareGroupFolderView/ShareGroupFolderView.tsx index 6235b3b..9cf6c84 100644 --- a/src/components/ShareGroup/ShareGroupFolderView/ShareGroupFolderView.tsx +++ b/src/components/ShareGroup/ShareGroupFolderView/ShareGroupFolderView.tsx @@ -7,7 +7,7 @@ import { useRecoilState } from 'recoil'; import { shareGroupMemberListState } from 'recoil/states/share_group'; const ShareGroupFolderView: React.FC = () => { - const [items, setItems] = useRecoilState(shareGroupMemberListState); + const [items] = useRecoilState(shareGroupMemberListState); return ( diff --git a/src/components/ShareGroup/ShareGroupImageItem/Styles.ts b/src/components/ShareGroup/ShareGroupImageItem/Styles.ts index 45e6734..2ea950a 100644 --- a/src/components/ShareGroup/ShareGroupImageItem/Styles.ts +++ b/src/components/ShareGroup/ShareGroupImageItem/Styles.ts @@ -3,8 +3,8 @@ import styled from 'styled-components'; export const Layout = styled.div<{ selected: boolean }>` border: ${({ selected, theme }) => selected ? `5px solid ${theme.colors.accent}` : '1px solid #fff'}; - width: 8rem; - height: 6rem; + width: 10rem; + height: 8rem; border-radius: 18px; `; diff --git a/src/components/ShareGroup/ShareGroupListItem/ShareGroupListItem.tsx b/src/components/ShareGroup/ShareGroupListItem/ShareGroupListItem.tsx index 073f7f8..06e10ac 100644 --- a/src/components/ShareGroup/ShareGroupListItem/ShareGroupListItem.tsx +++ b/src/components/ShareGroup/ShareGroupListItem/ShareGroupListItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import * as S from './Styles'; +import defaultProfile from '../../../assets/samples/emptyProfile.png'; interface ListProps { name: string; @@ -17,10 +18,10 @@ const ShareGroupListItem: React.FC = ({ return ( - {image !== '' ? ( + {image ? ( ) : ( - + )} = ({ items }) => { + const setSelectedGroup = useSetRecoilState(selectedGroupName); return ( {items.map((item) => ( setSelectedGroup(item.name)} > diff --git a/src/components/ShareGroup/ShareGroupRectButton/Styles.ts b/src/components/ShareGroup/ShareGroupRectButton/Styles.ts index da46f9a..fc5c709 100644 --- a/src/components/ShareGroup/ShareGroupRectButton/Styles.ts +++ b/src/components/ShareGroup/ShareGroupRectButton/Styles.ts @@ -6,11 +6,11 @@ export const Layout = styled.div` flex-direction: column; align-items: center; justify-content: center; - background: red; `; export const Rect = styled(I.Rectangle)` position: absolute; + width: 45%; `; export const RectText = styled.p` diff --git a/src/components/ShareGroup/ShareGroupTopButton/ShareGroupTopButton.tsx b/src/components/ShareGroup/ShareGroupTopButton/ShareGroupTopButton.tsx index 0127ae2..ddf1e62 100644 --- a/src/components/ShareGroup/ShareGroupTopButton/ShareGroupTopButton.tsx +++ b/src/components/ShareGroup/ShareGroupTopButton/ShareGroupTopButton.tsx @@ -1,18 +1,25 @@ import React from 'react'; import * as S from './Styles'; +import { useParams } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { groupSelectorbyId } from 'recoil/selectors/sharegroup'; const ShareGroupTopButton: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const int = parseInt(id || '0'); + const group = useRecoilValue(groupSelectorbyId(int)); + return ( - 2024 졸업 전시 + {group.name} - 12 + {group.memberCount} - 1232.23.12 + {group.createdAt} diff --git a/src/components/ShareGroup/ShareGroupTopButton/Styles.ts b/src/components/ShareGroup/ShareGroupTopButton/Styles.ts index 4dbe49f..a5a4f86 100644 --- a/src/components/ShareGroup/ShareGroupTopButton/Styles.ts +++ b/src/components/ShareGroup/ShareGroupTopButton/Styles.ts @@ -17,7 +17,7 @@ export const Layout = styled.div` export const Container = styled.div` width: 100%; height: 100%; - padding: 0 1.25rem; + padding: 0 1rem; display: flex; flex-direction: column; justify-content: space-evenly; @@ -25,7 +25,7 @@ export const Container = styled.div` `; export const Title = styled.p` - font-size: 0.9rem; + font-size: 0.7rem; color: ${({ theme }) => theme.colors.tertiary}; font-weight: 600; `; diff --git a/src/components/Vote/EmptyVoteBox/EmptyVoteBox.tsx b/src/components/Vote/EmptyVoteBox/EmptyVoteBox.tsx index 75c3747..f01902d 100644 --- a/src/components/Vote/EmptyVoteBox/EmptyVoteBox.tsx +++ b/src/components/Vote/EmptyVoteBox/EmptyVoteBox.tsx @@ -14,7 +14,7 @@ const EmptyVoteBox: React.FC = () => { 아직 안건이 없어요. 새로운 안건을 추가해주세요. - + 안건 추가하기 diff --git a/src/components/Vote/VoteModal/VoteModal.tsx b/src/components/Vote/VoteModal/VoteModal.tsx index 9af83d2..c28dba9 100644 --- a/src/components/Vote/VoteModal/VoteModal.tsx +++ b/src/components/Vote/VoteModal/VoteModal.tsx @@ -1,14 +1,13 @@ import React, { useState } from 'react'; import * as S from './Styles'; import { CloseModal, NextArrow } from 'assets/icon'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { isModalOpen, selectedPic } from 'recoil/states/vote'; -import { ParticularAgendaVote } from 'apis/vote'; + const VoteModal = () => { const setIsOpen = useSetRecoilState(isModalOpen); - const data = useRecoilValue(selectedPic); + const [data, setData] = useRecoilState(selectedPic); const [comment, setComment] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = (e: React.ChangeEvent) => { setComment(e.target.value); @@ -16,33 +15,32 @@ const VoteModal = () => { const handleIconClick = () => { setIsOpen(false); }; - const handleSubmit = async () => { - setIsSubmitting(true); // API 호출 시작 - try { - // 투표할 내용 - const voteData = [ - { - comment: comment, - agendaPhotoId: data.pictureId, // 선택된 사진 ID - }, - ]; - - // 특정 안건에 대한 투표 API 호출 - await ParticularAgendaVote(data.pictureId, voteData); - console.log('Vote successfully submitted!'); - - setIsOpen(false); // 모달 닫기 - } catch (error) { - if (error instanceof Error) { - console.error('Error submitting vote:', error.message); - alert('투표 제출 중 오류가 발생했습니다. 다시 시도해 주세요.'); - } else { - console.error('Unknown error occurred:', error); - alert('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); - } - } finally { - setIsSubmitting(false); // API 호출 종료 - } + const handleSubmit = () => { + setData({ ...data, comment: comment }); + setIsOpen(false); + // try { + // // 투표할 내용 + // const voteData = [ + // { + // comment: comment, + // agendaPhotoId: data.pictureId, // 선택된 사진 ID + // }, + // ]; + // // 특정 안건에 대한 투표 API 호출 + // await ParticularAgendaVote(data.pictureId, voteData); + // console.log('Vote successfully submitted!'); + // setIsOpen(false); // 모달 닫기 + // } catch (error) { + // if (error instanceof Error) { + // console.error('Error submitting vote:', error.message); + // alert('투표 제출 중 오류가 발생했습니다. 다시 시도해 주세요.'); + // } else { + // console.error('Unknown error occurred:', error); + // alert('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); + // } + // } finally { + // setIsSubmitting(false); // API 호출 종료 + // } }; return ( @@ -59,7 +57,7 @@ const VoteModal = () => { onChange={handleChange} value={comment} /> - + 투표 하기 diff --git a/src/components/navigationbar/NavigationBar.tsx b/src/components/navigationbar/NavigationBar.tsx index d90fc5f..7156869 100644 --- a/src/components/navigationbar/NavigationBar.tsx +++ b/src/components/navigationbar/NavigationBar.tsx @@ -1,17 +1,41 @@ -import React from 'react'; +import React, { useState } from 'react'; import * as S from './Styles'; import { AddBtn, HomeBtn, NotificationBtn } from 'assets/icon'; +import ShareGroupRectButton from 'components/ShareGroup/ShareGroupRectButton/ShareGroupRectButton'; +import { getHasSamplePhoto } from 'apis/getMembers'; +import { useNavigate } from 'react-router-dom'; const NavigationBar = () => { + const [isClicked, setIsClicked] = useState(true); + const navigate = useNavigate(); + const handleButtonClick = (add?: boolean) => { + getHasSamplePhoto().then((res) => { + if (!res) navigate('/login/profile'); + else { + if (add) navigate('add/member'); + else navigate('join'); + } + }); + }; return ( - + setIsClicked(!isClicked)}> + {isClicked && ( + + handleButtonClick()}> + + + handleButtonClick()}> + + + + )} ); }; diff --git a/src/components/navigationbar/Styles.ts b/src/components/navigationbar/Styles.ts index caf1200..8136503 100644 --- a/src/components/navigationbar/Styles.ts +++ b/src/components/navigationbar/Styles.ts @@ -1,3 +1,4 @@ +import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; export const Layout = styled.div` @@ -25,7 +26,20 @@ export const IconLayout = styled.div` gap: 30%; `; -export const AddButtonBox = styled.div` - width: 10%; +export const AddButtonBox = styled.button` position: relative; `; + +export const RectContainer = styled.div` + position: absolute; + bottom: 6rem; + width: 100%; + display: flex; + justify-content: space-evenly; + gap: 1rem; + align-items: center; +`; + +export const RectBox = styled.div` + cursor: pointer; +`; diff --git a/src/pages/EnterMain/EnterGuide/EnterGuide.tsx b/src/pages/EnterMain/EnterGuide/EnterGuide.tsx index 42cb1ec..2126c96 100644 --- a/src/pages/EnterMain/EnterGuide/EnterGuide.tsx +++ b/src/pages/EnterMain/EnterGuide/EnterGuide.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import * as S from './Styles'; import sky from '../../../assets/background/sky_dark.png'; import * as I from 'assets/icon'; @@ -6,6 +6,23 @@ import { useNavigate } from 'react-router-dom'; const EnterGuide = () => { const navigate = useNavigate(); + const [file, setFile] = useState(); + + const handleAddButtonClick = () => { + const fileInput = document.getElementById('file') as HTMLInputElement; + fileInput.click(); + }; + + const handleChangeFile = (event: React.ChangeEvent) => { + const fileList = event?.target.files; + setFile(fileList); + }; + + useEffect(() => { + if (file) { + navigate('add', { state: { file } }); + } + }, [file]); return ( <> @@ -37,8 +54,17 @@ const EnterGuide = () => { 위 가이드 라인을 준수하는 사진을
2장 이상 추가해주세요. - navigate('add')} /> - 사진 추가하기 +
+ + + 사진 추가하기 +
diff --git a/src/pages/EnterMain/EnterMain.tsx b/src/pages/EnterMain/EnterMain.tsx index ef39b1c..f9f0cb0 100644 --- a/src/pages/EnterMain/EnterMain.tsx +++ b/src/pages/EnterMain/EnterMain.tsx @@ -1,11 +1,22 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import * as S from './Styles'; import sky from '../../assets/background/sky.png'; import TypoBlurred from '../../assets/logo/typo-blurred.png'; import symbol from '../../assets/logo/symbol.png'; -import { Link, Outlet } from 'react-router-dom'; +import { Link, Outlet, useNavigate } from 'react-router-dom'; +import { getMyInfo } from 'apis/getMyInfo'; +import { useRecoilState } from 'recoil'; +import { UserState } from 'recoil/states/enter'; const EnterMain = () => { + const [userInfo, setUserInfo] = useRecoilState(UserState); + const navigate = useNavigate(); + useEffect(() => { + if (!userInfo) { + getMyInfo().then((res) => setUserInfo(res.data)); + } + navigate('/group'); + }, []); return ( <> diff --git a/src/pages/EnterMain/EnterPhoto/EnterPhoto.tsx b/src/pages/EnterMain/EnterPhoto/EnterPhoto.tsx index 568a953..974c58c 100644 --- a/src/pages/EnterMain/EnterPhoto/EnterPhoto.tsx +++ b/src/pages/EnterMain/EnterPhoto/EnterPhoto.tsx @@ -1,8 +1,97 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import * as S from './Styles'; import sky from '../../../assets/background/sky_dark.png'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { postPresignedUrl } from 'apis/postPresignedUrl'; +import axios from 'axios'; +import { postPhotoUpload } from 'apis/postPhotoUpload'; + +interface responseProp { + photoName: string; + photoUrl: string; + preSignedUrl: string; +} const EnterPhoto = () => { + const navigate = useNavigate(); + const location = useLocation(); + const state = Array.from(location.state?.file as File[]); + const [files, setFiles] = useState(state); + const [previews, setPreviews] = useState([]); + const [response, setResponse] = useState([]); + const [photoUrl, setPhotoUrl] = useState([]); + + const handleCloseClick = (id: number) => { + if (files) { + const newFiles = files.filter((_: any, index: number) => index !== id); + setFiles(newFiles); + } + }; + + const handleAddButtonClick = () => { + const fileInput = document.getElementById('file') as HTMLInputElement; + fileInput.click(); + }; + + const handleChangeFile = (event: React.ChangeEvent) => { + const fileList = event?.target.files; + if (files && fileList) { + const newFiles = files.concat(Array.from(fileList)); // 기존 파일과 새로운 파일 병합 + if (newFiles.length > 2) { + alert('사진 등록 개수를 초과했어요!'); + return; + } + setFiles(newFiles); + } + }; + + const handleSubmit = async () => { + if (files && files.length < 2) alert('사진을 두 장 이상 선택하세요!'); + else if (files) { + const nameList = files.map((file: File) => file.name); + if (nameList) { + const preSignedData = { + photoNameList: nameList, + }; + try { + const { data } = await postPresignedUrl(preSignedData); + const photoUrls = data.preSignedUrlInfoList.map( + (item: any) => item.photoUrl, + ); + setResponse(data.preSignedUrlInfoList); + setPhotoUrl(photoUrls); + const uploadPromises = files.map(async (fileItem, index) => { + const presignedUrl = response[index]?.preSignedUrl; + if (presignedUrl) { + await axios.put(presignedUrl, fileItem, { + headers: { + 'Content-Type': fileItem.type, // 파일의 MIME 타입 설정 + }, + withCredentials: true, + }); + } + }); + await Promise.all(uploadPromises); // 모든 업로드가 완료될 때까지 대기 + const requestData = { + photoUrlList: photoUrl || [], + }; + postPhotoUpload(requestData); + } catch (error) { + console.error('Error: ', error); + } + navigate('/'); + } + } + }; + + useEffect(() => { + if (files) { + const array = Array.from(files) || []; + const newPreview = array.map((fl: any) => URL.createObjectURL(fl)); + setPreviews(newPreview); + } + }, [files]); + return ( <> @@ -11,20 +100,31 @@ const EnterPhoto = () => { 정면, 측면 사진을 각각 한 장씩 추가 해주세요. + - - - - - - + {previews.map((p, idx) => ( + + + handleCloseClick(idx)} /> + + ))} + {previews.length < 2 ? : null} + {previews.length < 1 ? : null} - - - 사진 추가 + + + + 사진 추가 + - - 사진 선택 완료 + + 사진 선택 완료 ); diff --git a/src/pages/EnterMain/EnterPhoto/Styles.ts b/src/pages/EnterMain/EnterPhoto/Styles.ts index 0f21e3b..0792b5f 100644 --- a/src/pages/EnterMain/EnterPhoto/Styles.ts +++ b/src/pages/EnterMain/EnterPhoto/Styles.ts @@ -53,17 +53,25 @@ export const GuideContainer = styled.div` export const GuideBox = styled.div` width: 6rem; height: 6rem; - border-radius: 5%; position: relative; + border-radius: 5%; background-color: gray; `; +export const GuideImg = styled.img` + width: 100%; + height: 100%; + border-radius: 5%; + object-fit: cover; +`; + export const CloseBtn = styled(I.CloseModal)` - width: 12%; position: absolute; - top: -12%; + top: -10%; right: 0; cursor: pointer; + width: 15%; + z-index: 100; `; export const SubmitBtn = styled(I.Buttonrect)` @@ -90,6 +98,7 @@ export const PhotoAddBtn = styled(I.ButtonSmall)` width: 25%; top: 53%; right: 15%; + cursor: pointer; `; export const PhotoAddText = styled.div` @@ -100,6 +109,7 @@ export const PhotoAddText = styled.div` color: #4879af; font-weight: bolder; font-size: 0.8rem; + cursor: pointer; `; export const PhotoPlus = styled(I.AddSmall)` diff --git a/src/pages/EnterMain/EnterProfile/EnterProfile.tsx b/src/pages/EnterMain/EnterProfile/EnterProfile.tsx index e1aa26a..96f5803 100644 --- a/src/pages/EnterMain/EnterProfile/EnterProfile.tsx +++ b/src/pages/EnterMain/EnterProfile/EnterProfile.tsx @@ -2,32 +2,35 @@ import React from 'react'; import * as S from './Styles'; import skydark from '../../../assets/background/sky_dark.png'; import { Link, Outlet } from 'react-router-dom'; +import { useRecoilState } from 'recoil'; +import { UserState } from 'recoil/states/enter'; +import { getMyInfo } from 'apis/getMyInfo'; const EnterProfile = () => { + const [user, setUserInfo] = useRecoilState(UserState); + if (!user) { + getMyInfo().then((res) => setUserInfo(res.data)); + } return ( <> - - - -
이메일
- -
이름
-
-
- - -
사진 분류를 위해 인공지능을 학습 시킬 거에요.
-
학습에 필요한 얼굴 사진을 추가해 주세요.
-
- - -
사진 추가하기
-
- -
+ + + {user.email} + {user.name} + + + +
사진 분류를 위해 인공지능을 학습 시킬 거에요.
+
학습에 필요한 얼굴 사진을 추가해 주세요.
+
+ + +
사진 추가하기
+
+
diff --git a/src/pages/EnterMain/EnterProfile/Styles.ts b/src/pages/EnterMain/EnterProfile/Styles.ts index 0a79594..a1321c9 100644 --- a/src/pages/EnterMain/EnterProfile/Styles.ts +++ b/src/pages/EnterMain/EnterProfile/Styles.ts @@ -4,18 +4,12 @@ import * as I from 'assets/icon'; export const Layout = styled.div` width: 100%; height: 100%; - z-index: 40; `; export const Background = styled.img` width: 100%; height: 100%; position: absolute; - z-index: 40; -`; - -export const ProfileContainer = styled.div` - z-index: 40; `; export const Name = styled.div` @@ -24,7 +18,6 @@ export const Name = styled.div` `; export const ProfileBox = styled.div` - z-index: 40; color: white; position: absolute; top: 48%; @@ -35,19 +28,21 @@ export const ProfileBox = styled.div` transform: translate(-50%, -50%); `; -export const Profile = styled(I.Profile)` - z-index: 40; +export const Profile = styled.img` position: absolute; - width: 35%; - height: 100%; + width: 7rem; + height: 7rem; + border: 2px solid white; + border-radius: 50%; top: 37%; left: 50%; transform: translate(-50%, -50%); + object-fit: cover; `; export const ProfileLine = styled(I.Line_star)` - z-index: 40; position: absolute; + width: 80%; top: 55%; left: 50%; transform: translate(-50%, -50%); @@ -59,7 +54,6 @@ export const ProfileGuide = styled.div` top: 60%; left: 50%; transform: translate(-50%, -50%); - z-index: 40; font-size: 15px; color: white; text-align: center; @@ -69,24 +63,23 @@ export const ProfileGuide = styled.div` `; export const PhotoAdd = styled(I.Buttonrect)` - z-index: 40; position: absolute; + width: 55%; top: 70%; left: 50%; transform: translate(-50%, -50%); `; export const PhotoPlus = styled(I.AddBtn)` - z-index: 40; position: absolute; + width: 7%; top: 70%; - left: 50%; + left: 54%; transform: translate(150%, -50%); cursor: pointer; `; export const PhotoAddText = styled.div` - z-index: 40; position: absolute; color: #4879af; top: 70%; diff --git a/src/pages/ShareGroup/ShareGroupFolder/ShareGroupFolder.tsx b/src/pages/ShareGroup/ShareGroupFolder/ShareGroupFolder.tsx index eb85ace..79524a9 100644 --- a/src/pages/ShareGroup/ShareGroupFolder/ShareGroupFolder.tsx +++ b/src/pages/ShareGroup/ShareGroupFolder/ShareGroupFolder.tsx @@ -1,16 +1,24 @@ // Share Group 1,2페이지 레이아웃 -import React, { useState } from 'react'; +import React, { useEffect } from 'react'; import Header from 'components/Header/Header'; import * as S from './Styles'; import ShareGroupFolderView from 'components/ShareGroup/ShareGroupFolderView/ShareGroupFolderView'; import { useParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { shareGroupMemberListState } from 'recoil/states/share_group'; +import { getShareGroupMembers } from 'apis/getMyShareGroup'; const ShareGroupFolder: React.FC = () => { const { id } = useParams<{ id: string }>(); + const setShareGroupMember = useSetRecoilState(shareGroupMemberListState); - const getShareGroup = async (id: string): Promise => { - // shareGroupId로 api call - }; + useEffect(() => { + getShareGroupMembers(Number(id)).then((res) => { + const { profileInfoList } = res; + if (profileInfoList === null) return; + setShareGroupMember(profileInfoList); + }); + }, [id]); return ( diff --git a/src/pages/ShareGroup/ShareGroupMain/ShareGroupMain.tsx b/src/pages/ShareGroup/ShareGroupMain/ShareGroupMain.tsx index 22eb918..37b1292 100644 --- a/src/pages/ShareGroup/ShareGroupMain/ShareGroupMain.tsx +++ b/src/pages/ShareGroup/ShareGroupMain/ShareGroupMain.tsx @@ -7,15 +7,17 @@ import ShareGruopListView from 'components/ShareGroup/ShareGroupListView/ShareGr import ShareGroupAddButton from 'components/ShareGroup/ShareGroupAddButton/ShareGroupAddButton'; import { useRecoilState } from 'recoil'; import { shareGroupListState } from 'recoil/states/share_group'; +import { getMyShareGroup } from 'apis/getMyShareGroup'; const ShareGroupMain: React.FC = () => { - // 회원 정보를 바탕으로 공유 그룹 리스트를 가져와야 함' const [shareGroupList, setShareGroup] = useRecoilState(shareGroupListState); const [showButton, setShowButton] = useState(false); useEffect(() => { - // 공유 그룹 리스트를 가져옴 - // setShareGroup(shareGroupList); + getMyShareGroup().then((res) => { + if (res === null) return; + setShareGroup(res); + }); }, []); const handleClick = () => { diff --git a/src/pages/Vote/Styles.ts b/src/pages/Vote/Styles.ts index c0d304b..9305d08 100644 --- a/src/pages/Vote/Styles.ts +++ b/src/pages/Vote/Styles.ts @@ -30,3 +30,9 @@ export const BackLayout = styled.div` background: rgba(0, 0, 0, 0.5); z-index: 2; `; + +export const DropDownBox = styled.div` + position: absolute; + top: 10%; + left: 0; +`; diff --git a/src/pages/Vote/VoteMainPage.tsx b/src/pages/Vote/VoteMainPage.tsx index a9de0ab..bbdd2d2 100644 --- a/src/pages/Vote/VoteMainPage.tsx +++ b/src/pages/Vote/VoteMainPage.tsx @@ -40,8 +40,10 @@ const VoteMainPage = () => { {(isOpen || isAlerted) && } {isAlerted && } -
- +
+ + + {component} ); diff --git a/src/pages/Vote/VotePage/Styles.ts b/src/pages/Vote/VotePage/Styles.ts index 086405d..95e7291 100644 --- a/src/pages/Vote/VotePage/Styles.ts +++ b/src/pages/Vote/VotePage/Styles.ts @@ -1,3 +1,4 @@ +import { CloseBtnRound } from 'assets/icon'; import styled from 'styled-components'; export const Layout = styled.div` @@ -8,19 +9,64 @@ export const Layout = styled.div` gap: 1.5rem 0.5rem; `; -export const ImgLayout = styled.div` +export const Container = styled.div` width: 8rem; position: relative; - border-radius: 0.5rem; - overflow: hidden; `; -export const ImgBox = styled.img` +export const ImgLayout = styled.div` width: 8rem; height: 6rem; + border-radius: 0.9rem; + border: 2px solid white; + overflow: hidden; +`; + +export const ImgBox = styled.img` + width: 100%; + height: 100%; + cursor: pointer; + object-fit: cover; +`; + +export const VoterLayout = styled.div<{ click?: boolean }>` + width: 100%; + display: flex; + align-items: center; + position: absolute; + bottom: -10%; + border-radius: 1.6rem; + background: ${(props) => + props.click ? 'rgba(255, 255, 255, 0.92)' : 'none'}; + backdrop-filter: ${(props) => (props.click ? 'blur(2px)' : 'none')}; +`; + +export const VoterContainer = styled.div<{ click?: boolean }>` + display: ${(props) => (props.click ? 'block' : 'none')}; + width: 5rem; + height: 100%; + padding-left: 0.2rem; + color: #1d3a72; + font-size: 0.7rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const VoterBox = styled.img` + width: 1.9rem; + height: 1.9rem; + border-radius: 50%; + border: 2px solid white; cursor: pointer; `; +export const CloseButton = styled(CloseBtnRound)` + position: absolute; + right: 0.3rem; +`; + export const ButtonLayout = styled.button` width: 90%; position: absolute; diff --git a/src/pages/Vote/VotePage/VotePage.tsx b/src/pages/Vote/VotePage/VotePage.tsx index da4eb49..ce49c2e 100644 --- a/src/pages/Vote/VotePage/VotePage.tsx +++ b/src/pages/Vote/VotePage/VotePage.tsx @@ -1,12 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; import * as S from './Styles'; import VoteTitle from 'components/Vote/VoteTitle/VoteTitle'; import { CloudNextBtn } from 'assets/icon'; import VoteModal from 'components/Vote/VoteModal/VoteModal'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; import { isModalOpen, selectedPic, registeredPics } from 'recoil/states/vote'; -import VoterBox from 'components/Vote/VoterBox/VoterBox'; import { useLocation, useNavigate } from 'react-router-dom'; +import { UserState } from 'recoil/states/enter'; const VotePage = () => { const [isOpen, setIsOpen] = useRecoilState(isModalOpen); @@ -14,7 +14,11 @@ const VotePage = () => { const location = useLocation(); const title = location.state?.title; const pictures = useRecoilValue(registeredPics); - const setSelectedPic = useSetRecoilState(selectedPic); + const [selectedPicture, setSelectedPic] = useRecoilState(selectedPic); + const resetSelect = useResetRecoilState(selectedPic); + const profile = useRecoilValue(UserState); + const [click, setClick] = useState(false); + const handleClickBtn = () => { navigate('/vote/list'); }; @@ -22,15 +26,34 @@ const VotePage = () => { setIsOpen(true); setSelectedPic(pictures[idx]); }; + const handleCloseClick = () => { + resetSelect(); + }; + return ( <> {isOpen && } {pictures.map((pic, idx) => ( - - handleImgClick(idx)} /> - + + + handleImgClick(idx)} /> + + {selectedPicture.comment && + selectedPicture.pictureId === pic.pictureId && ( + + setClick(!click)} + /> + + {selectedPicture.comment} + + + + )} + ))} diff --git a/src/recoil/selectors/sharegroup.ts b/src/recoil/selectors/sharegroup.ts new file mode 100644 index 0000000..162eabb --- /dev/null +++ b/src/recoil/selectors/sharegroup.ts @@ -0,0 +1,13 @@ +import { selectorFamily } from 'recoil'; +import { shareGroupListState } from 'recoil/states/share_group'; + +export const groupSelectorbyId = selectorFamily({ + key: 'groupSelectorbyId', + get: + (id: number) => + ({ get }) => { + const groups = get(shareGroupListState); + const selectedGroup = groups.filter((val) => val.shareGroupId === id); + return selectedGroup[0]; + }, +}); diff --git a/src/recoil/states/enter.ts b/src/recoil/states/enter.ts index 7efb48f..0d6c1ff 100644 --- a/src/recoil/states/enter.ts +++ b/src/recoil/states/enter.ts @@ -1,4 +1,11 @@ import { atom } from 'recoil'; +import { UserStateType } from 'recoil/types/enter'; +import { recoilPersist } from 'recoil-persist'; + +const { persistAtom } = recoilPersist({ + key: 'persist-atom-key', + storage: localStorage, +}); export const loginState = atom({ key: 'loginState', @@ -9,3 +16,9 @@ export const clauseState = atom({ key: 'clauseState', default: { isClauseIn: false }, }); + +export const UserState = atom({ + key: 'UserState', + default: undefined, + effects_UNSTABLE: [persistAtom], +}); diff --git a/src/recoil/states/share_group.ts b/src/recoil/states/share_group.ts index c598d0f..549646f 100644 --- a/src/recoil/states/share_group.ts +++ b/src/recoil/states/share_group.ts @@ -36,42 +36,14 @@ export const selectedImageState = atom({ default: null, }); -export const shareGroupListState = atom({ +export const shareGroupListState = atom({ key: 'shareGroupListState', - default: [ - { - shareGroupId: 1, - name: '못 갈 뻔하다가 겨우 간 우리의 뜨거운 보라카이 여행', - image: 'https://i.imgur.com/GfKSahj.jpeg', - memberCount: 3, - createdAt: '2021-08-18', - inviteUrl: '', - }, - ], + default: [], }); export const shareGroupMemberListState = atom({ key: 'shareGroupMemberList', - default: [ - { - profileId: 1, - name: '한석봉', - image: 'https://avatars.githubusercontent.com/u/6400346?v=4', - memberId: 1, - }, - { - profileId: 2, - name: '김동현', - image: 'https://avatars.githubusercontent.com/u/6400346?v=4', - memberId: 2, - }, - { - profileId: 3, - name: '김민수', - image: 'https://avatars.githubusercontent.com/u/6400346?v=4', - memberId: 3, - }, - ], + default: [], }); export const shareGroupDetailSelectedImageState = atom( @@ -108,3 +80,8 @@ export const shareGroupDetailSelectedImageState = atom( }, }, ); + +export const selectedGroupName = atom({ + key: 'selectedGroupName', + default: '', +}); diff --git a/src/recoil/types/enter.ts b/src/recoil/types/enter.ts new file mode 100644 index 0000000..be3935d --- /dev/null +++ b/src/recoil/types/enter.ts @@ -0,0 +1,6 @@ +export interface UserStateType { + name: string; + email: string; + image: string; + memberId: number; // memberId 추가 +} diff --git a/src/recoil/types/members.ts b/src/recoil/types/members.ts index 5990e33..ededbf2 100644 --- a/src/recoil/types/members.ts +++ b/src/recoil/types/members.ts @@ -28,3 +28,12 @@ export interface deleteResponse { deleted_at: string; }; } + +export interface samplePhotoResponse { + status: number; + code: string; + message: string; + data: { + hasSamplePhoto: boolean; + }; +} diff --git a/src/recoil/types/vote.ts b/src/recoil/types/vote.ts index 1863840..b1f0a22 100644 --- a/src/recoil/types/vote.ts +++ b/src/recoil/types/vote.ts @@ -31,6 +31,7 @@ export interface profileInfo { export interface registeredPicsType { pictureId: number; url: string; + comment?: string; } export interface PostApiResponse { diff --git a/src/utils/UseCarousel.tsx b/src/utils/UseCarousel.tsx index 1b259fb..2928679 100644 --- a/src/utils/UseCarousel.tsx +++ b/src/utils/UseCarousel.tsx @@ -12,7 +12,7 @@ export const useCarousel = ( const updateOffset = useCallback(() => { if (containerRef.current) { const containerWidth = containerRef.current.offsetWidth; - const newOffset = -currentIndex * containerWidth + currentIndex * 124.5; + const newOffset = -currentIndex * containerWidth * 0.65; setOffset(newOffset); }