diff --git a/package-lock.json b/package-lock.json index 2556a74..c16d72a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.19.2", + "jszip": "^3.10.1", "react": "^18.3.1", "react-cookie": "^7.2.0", "react-dom": "^18.3.1", @@ -9190,6 +9191,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -11003,6 +11009,49 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11080,6 +11129,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -11838,6 +11895,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -14774,6 +14836,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/package.json b/package.json index c9ab72e..2535b43 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.19.2", + "jszip": "^3.10.1", "react": "^18.3.1", "react-cookie": "^7.2.0", "react-dom": "^18.3.1", diff --git a/src/components/ShareGroup/ShareGroupImageList/ShareGroupImageList.tsx b/src/components/ShareGroup/ShareGroupImageList/ShareGroupImageList.tsx index 025880d..88aea6f 100644 --- a/src/components/ShareGroup/ShareGroupImageList/ShareGroupImageList.tsx +++ b/src/components/ShareGroup/ShareGroupImageList/ShareGroupImageList.tsx @@ -7,12 +7,16 @@ import { isModalState, selectedImageState, } from 'recoil/states/share_group'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import ShareGroupBottomBar from '../ShareGroupBottomBar/ShareGroupBottomBar'; import { deletePhoto } from 'apis/deletePhoto'; import { useLocation, useNavigate } from 'react-router-dom'; import { getPhotos, getPhotosAll, getPhotosEtc } from 'apis/getPhotos'; -import { addedAgendaPhotos, addedAgendaSrcs } from 'recoil/states/vote'; +import { + addedAgendaPhotos, + addedAgendaSrcs, + choiceMode, +} from 'recoil/states/vote'; export interface itemProp { createdAt: string; @@ -53,7 +57,7 @@ const ShareGroupImageList = ({ const [hasMore, setHasMore] = useState(true); // 사진 칸 observer const containerRef = useRef(null); - const choiceMode = state.choiceMode; + const mode: boolean = useRecoilValue(choiceMode); const nav = useNavigate(); const [photos, setPhotos] = useRecoilState(addedAgendaPhotos); const [sources, setSources] = useRecoilState(addedAgendaSrcs); @@ -75,7 +79,7 @@ const ShareGroupImageList = ({ ...sources, ...checkedImg.filter((id) => !sources.includes(id)), ]; - if (choiceMode && newPhotos.length > 6) { + if (mode && newPhotos.length > 6) { alert('사진의 최대 등록 개수는 6장입니다'); nav(-1); return; @@ -127,7 +131,7 @@ const ShareGroupImageList = ({ }; useEffect(() => { - if (choiceMode) setIsChecked(true); + if (mode) setIsChecked(true); if (!isChecked) setCheckedImg([]); }, [isChecked]); @@ -240,7 +244,7 @@ const ShareGroupImageList = ({ ))} - {choiceMode ? ( + {mode ? ( <> {checkedImg.length > 0 ? ( diff --git a/src/components/Vote/PhotoContainer/PhotoAddBtn.tsx b/src/components/Vote/PhotoContainer/PhotoAddBtn.tsx index 4b8747a..67d76da 100644 --- a/src/components/Vote/PhotoContainer/PhotoAddBtn.tsx +++ b/src/components/Vote/PhotoContainer/PhotoAddBtn.tsx @@ -15,6 +15,7 @@ import { filteredProfile, profile, } from 'pages/ShareGroup/ShareGroupFolder/ShareGroupFolder'; +import { choiceMode } from 'recoil/states/vote'; const PhotoAddBtn = () => { const nav = useNavigate(); @@ -25,6 +26,7 @@ const PhotoAddBtn = () => { const setPhotoRequest = useSetRecoilState(photoRequestState); const setType = useSetRecoilState(photoTypeState); const setShareGroupMember = useSetRecoilState(shareGroupMemberListState); + const [mode, setMode] = useRecoilState(choiceMode); useEffect(() => { getShareGroupMembers(groupId).then((res) => { @@ -74,9 +76,8 @@ const PhotoAddBtn = () => { } const data = { shareGroupId: groupId, profileId: id, size: 20 }; setPhotoRequest(data); - nav('/group/detail', { - state: { choiceMode: true }, - }); + setMode(true); + nav(`/group/${groupId}/${id}`); }; return ( diff --git a/src/pages/ShareGroup/ShareGroupDetailPage/DropDown.tsx b/src/pages/ShareGroup/ShareGroupDetailPage/DropDown.tsx index eaa4d80..a8b0a5f 100644 --- a/src/pages/ShareGroup/ShareGroupDetailPage/DropDown.tsx +++ b/src/pages/ShareGroup/ShareGroupDetailPage/DropDown.tsx @@ -8,13 +8,12 @@ import { photoTypeState, shareGroupMemberListState, } from 'recoil/states/share_group'; +import { useNavigate, useParams } from 'react-router-dom'; -interface DropDownProps { - groupId: number; -} - -const DropDown: React.FC = ({ groupId }) => { +const DropDown: React.FC = () => { const [isClicked, setIsClicked] = useState(false); + const { id, profileId } = useParams(); + const navigator = useNavigate(); const members = useRecoilValue(shareGroupMemberListState); const names = members ?.filter((mem) => mem.memberId !== null) @@ -29,7 +28,8 @@ const DropDown: React.FC = ({ groupId }) => { setIsClicked(!isClicked); }; - const handleItemClick = (idx: number, profileId: number, name: string) => { + const handleItemClick = (idx: number, name: string) => { + navigator(`/group/${id}/${names[idx].profileId}`); if (name === '모든 사진') { setPhotoType('all'); } else if (name === '기타 사진') { @@ -39,7 +39,11 @@ const DropDown: React.FC = ({ groupId }) => { } setIsClicked(false); setTitle(names[idx].name); - const newData = { shareGroupId: groupId, profileId: profileId, size: 20 }; + const newData = { + shareGroupId: Number(id), + profileId: Number(profileId), + size: 20, + }; setRequestState(newData); }; @@ -55,7 +59,7 @@ const DropDown: React.FC = ({ groupId }) => { names[i].name === title ? ( handleItemClick(i, name.profileId, name.name)} + onClick={() => handleItemClick(i, name.name)} style={{ fontWeight: '700' }} > {name.name} @@ -63,7 +67,7 @@ const DropDown: React.FC = ({ groupId }) => { ) : ( handleItemClick(i, name.profileId, name.name)} + onClick={() => handleItemClick(i, name.name)} > {name.name} diff --git a/src/pages/ShareGroup/ShareGroupDetailPage/ShareGroupDetailPage.tsx b/src/pages/ShareGroup/ShareGroupDetailPage/ShareGroupDetailPage.tsx index 70d8b4c..9632bbe 100644 --- a/src/pages/ShareGroup/ShareGroupDetailPage/ShareGroupDetailPage.tsx +++ b/src/pages/ShareGroup/ShareGroupDetailPage/ShareGroupDetailPage.tsx @@ -14,13 +14,13 @@ import { shareGroupId, } from 'recoil/states/share_group'; import { getPhotos, getPhotosAll, getPhotosEtc } from 'apis/getPhotos'; +import { choiceMode } from 'recoil/states/vote'; const ShareGroupDetailPage: React.FC = () => { const [isLoading, setIsLoading] = useState(false); const { id, profileId } = useParams<{ id: string; profileId: string }>(); const nav = useNavigate(); - const location = useLocation(); - const mode = location.state.choiceMode; + const mode = useRecoilValue(choiceMode); const groupId = useRecoilValue(shareGroupId); const [requestData, setRequestData] = useRecoilState(photoRequestState); const requestType = useRecoilValue(photoTypeState); @@ -101,7 +101,7 @@ const ShareGroupDetailPage: React.FC = () => { - +
diff --git a/src/recoil/states/vote.ts b/src/recoil/states/vote.ts index 03a5da2..3b57ee6 100644 --- a/src/recoil/states/vote.ts +++ b/src/recoil/states/vote.ts @@ -96,3 +96,9 @@ export const addedAgendaSrcs = atom({ default: [], effects_UNSTABLE: [persistAtom], }); + +export const choiceMode = atom({ + key: 'choiceMode', + default: false, + effects_UNSTABLE: [persistAtom], +}); diff --git a/src/utils/ImageZipDownloader.ts b/src/utils/ImageZipDownloader.ts index 3d7abd3..86f61e7 100644 --- a/src/utils/ImageZipDownloader.ts +++ b/src/utils/ImageZipDownloader.ts @@ -1,24 +1,34 @@ -// 이미지들을 jpeg로 변환하여 다운로드 -const imageZipDownloader = async (imageUrls: string[]) => { - imageUrls.forEach((url, index) => { - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = url; - img.onload = () => { - // create Canvas - const canvas = document.createElement('canvas'); - const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d'); - if (!ctx) return; - canvas.width = img.width; - canvas.height = img.height; - ctx?.drawImage(img, 0, 0); - // for create tag anchor - const a = document.createElement('a'); - a.download = `image-${index}-download`; - a.href = canvas.toDataURL('image/jpeg'); - a.click(); - }; - }); +import JSZip from 'jszip'; + +// 이미지들을 jpeg로 변환하여 zip 파일로 다운로드 +const imageZipDownloader = async ({ + imageUrls, +}: { + imageUrls: string[]; +}): Promise => { + const zip = new JSZip(); + + for (const url of imageUrls) { + try { + const response = await fetch(url); + const blob = await response.blob(); + const fileName = url.split('/').pop() || 'image'; + zip.file(fileName, blob); + } catch (error) { + console.error('Error processing image:', error); + throw error; + } + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + const url = window.URL.createObjectURL(zipBlob); + const a = document.createElement('a'); + a.href = url; + a.download = 'images.zip'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); }; export default imageZipDownloader;