Skip to content

Commit

Permalink
Merge pull request #90 from Na-o-man/feat/#89
Browse files Browse the repository at this point in the history
Feat: 개별 사진 다운로드 구현 등 3개
  • Loading branch information
eomseona authored Aug 21, 2024
2 parents 6e4e629 + a540841 commit 127cfc4
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import React from 'react';
import * as S from './Styles';
import logo from '../../../assets/design/logo/symbol.png';
import imageZipDownloader from 'utils/ImageZipDownloader';

interface BottomBarProps {
symbol?: boolean;
button?: boolean;
delButton?: boolean;
onDelete?: () => void; // 삭제하기 버튼 클릭 시 호출될 함수
srcs: string[];
}

const ShareGroupBottomBar: React.FC<BottomBarProps> = ({
symbol,
button,
delButton,
onDelete,
srcs,
}) => {
// 선택한 이미지들의 URL을 다운로드함
const imageUrls: string[] = srcs;
const handleDownload = async (): Promise<void> => {
await imageZipDownloader({ imageUrls });
};
return (
<S.Layout>
<S.BottomBar />
{symbol && <S.Symbol src={logo} alt="logo" />}
{button && (
<S.FilledCloudButtonContainer>
<S.FilledCloudButtonContainer onClick={handleDownload}>
<S.FilledCloudButtonText>다운받기</S.FilledCloudButtonText>
<S.FilledCloudButton />
</S.FilledCloudButtonContainer>
Expand Down
1 change: 0 additions & 1 deletion src/components/ShareGroup/ShareGroupCarousel/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const CarouselTrack = styled.div<{
`;

export const CarouselBlankItem = styled.div<{ isRight?: boolean }>`
background: red;
margin-right: ${(props) => (props.isRight ? '18%' : 0)};
margin-left: ${(props) => (props.isRight ? 0 : '18%')};
`;
Expand Down
7 changes: 5 additions & 2 deletions src/components/ShareGroup/ShareGroupFolderView/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export const Layout = styled.div`
`;

export const TopContainer = styled.div`
width: 100%;
width: 67.5%;
margin: 0 auto;
position: relative;
left: 50%;
display: flex;
justify-content: end;
align-items: center;
`;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import * as S from './Styles';
import ShareGroupImageItem from '../ShareGroupImageItem/ShareGroupImageItem';
import ShareGroupModal from '../ShareGroupImageModal/ShareGroupImageModal';
Expand All @@ -11,6 +11,7 @@ import { useRecoilState } from 'recoil';
import ShareGroupBottomBar from '../ShareGroupBottomBar/ShareGroupBottomBar';
import { deletePhoto } from 'apis/deletePhoto';
import { useLocation, useNavigate } from 'react-router-dom';
import { check } from 'prettier';

export interface itemProp {
createdAt: string;
Expand All @@ -36,11 +37,17 @@ const ShareGroupImageList = ({
const [isModal, setIsModal] = useRecoilState(isModalState);
const [selectedImage, setSelectedImage] = useRecoilState(selectedImageState);
const [date, setDate] = useState<string>();
// infinite scroll을 위한 state
const [page, setPage] = useState<number>(0);
const [localItems, setLocalItems] = useState<itemProp[]>(items);
const [isChecked, setIsChecked] = useRecoilState(checkModeState);
const [checkedImg, setCheckedImg] = useState<number[]>([]);
const [srcs, setSrcs] = useState<string[]>([]);
// infinite scroll loading을 위한 state
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState<boolean>(true);
// 사진 칸 observer
const observer = useRef<IntersectionObserver | null>(null);
const choiceMode = state.choiceMode;
const nav = useNavigate();

Expand All @@ -67,19 +74,38 @@ const ShareGroupImageList = ({
setIsModal(false);
};

const handleNext = () => {
if (page > maxPage) return;
const nextPage = page + 1;
setPage(nextPage);
getApi(nextPage);
};
const fetchItems = useCallback(async () => {
if (loading || !hasMore) return;

const handlePrev = () => {
if (page < 1) return;
const prevPage = page - 1;
setPage(prevPage);
getApi(prevPage);
};
setLoading(true);
if (page > maxPage) {
setHasMore(false);
setLoading(false);
return;
}
await getApi(page + 1);
setPage((prev) => prev + 1);
setLoading(false);
}, [page, loading, hasMore]);

// infinite scroll의 다음 아이템을 가져올지 결정하는 함수
// 마지막 아이템이 뷰포트에 들어오면 fetchItems 함수를 호출
const lastItemRef = useCallback(
(node: HTMLDivElement | null) => {
if (loading) return;

if (observer.current) observer.current.disconnect();

observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
fetchItems();
}
});

if (node) observer.current.observe(node);
},
[loading, hasMore, fetchItems],
);

// 사진 삭제
const handleDelete = async () => {
Expand Down Expand Up @@ -112,9 +138,14 @@ const ShareGroupImageList = ({
if (!isChecked) setCheckedImg([]);
}, [isChecked]);

// infinite scroll을 위한 useEffect
useEffect(() => {
fetchItems();
}, [fetchItems]);

return (
<>
<S.Layout isModal={isModal}>
<S.Layout isModal={isModal} ref={lastItemRef}>
<S.PhotoLayout>
{localItems.map((item, i) => (
<ShareGroupImageItem
Expand All @@ -130,14 +161,9 @@ const ShareGroupImageList = ({
))}
</S.PhotoLayout>
</S.Layout>
<S.PageContainer>
<S.PageBtn onClick={handlePrev}></S.PageBtn>
<S.Page>{page + 1 + ' / ' + maxPage}</S.Page>
<S.PageBtn onClick={handleNext}></S.PageBtn>
</S.PageContainer>
{choiceMode ? (
<>
<ShareGroupBottomBar />
<ShareGroupBottomBar srcs={srcs} />
{checkedImg.length > 0 ? (
<S.CloudButtonContainer
onClick={() => {
Expand All @@ -160,12 +186,22 @@ const ShareGroupImageList = ({
src={selectedImage}
onClose={handleCloseModal}
/>
<ShareGroupBottomBar button delButton onDelete={handleDelete} />
<ShareGroupBottomBar
button
delButton
onDelete={handleDelete}
srcs={srcs}
/>
</>
) : checkedImg.length > 0 ? (
<ShareGroupBottomBar button delButton onDelete={handleDelete} />
<ShareGroupBottomBar
button
delButton
onDelete={handleDelete}
srcs={srcs}
/>
) : (
<ShareGroupBottomBar symbol />
<ShareGroupBottomBar symbol srcs={srcs} />
)}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ShareGroup/ShareGroupImageList/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const Layout = styled.div<LayoutProps>`
justify-content: flex-start;
top: 6rem;
width: 90%;
height: 70%;
height: 75%;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ShareGroup/ShareGroupTopButton/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as I from '../../../assets/icon';

export const TopBtn = styled(I.TopBtn)`
position: absolute;
width: 40%;
width: 61.5%;
`;

export const Layout = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const ShareGroupDetailPage: React.FC = () => {
try {
if (isAllPhoto || requestData.profileId === 0) {
const { status, data } = await getPhotosAll(reqDataWithPage);
console.log(status, data);
if (status === 200) {
setItems(data.photoInfoList);
setMaxPage(data.totalPages);
Expand Down
94 changes: 73 additions & 21 deletions src/utils/ImageZipDownloader.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,86 @@
import JSZip from 'jszip';

// 모바일 장치인지 확인
const isMobile = (): boolean => {
if (window.matchMedia('(max-width: 1000px)').matches) {
return true;
}
// 두 번째 방법
const userAgent = navigator.userAgent;

// iOS, Android, Windows Phone 등의 모바일 장치를 감지
if (/android/i.test(userAgent)) {
return true;
}

if (/iPhone|iPad|iPod/i.test(userAgent)) {
return true;
}

if (/windows phone/i.test(userAgent)) {
return true;
}

// 기타 모바일 장치 감지
if (/mobile/i.test(userAgent)) {
return true;
}

return false;
};

// 이미지들을 jpeg로 변환하여 zip 파일로 다운로드
const imageZipDownloader = async ({
imageUrls,
}: {
imageUrls: string[];
}): Promise<void> => {
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;
if (isMobile()) {
// 모바일일 경우 개별 다운로드
for (const url of imageUrls) {
try {
const response = await fetch(url);
const blob = await response.blob();
const fileName = url.split('/').pop() || 'image';
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('Error processing image:', error);
throw error;
}
}
} else {
// 모바일이 아니면 ZIP 파일로 압축하여 다운로드
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);
const zipBlob = await zip.generateAsync({ type: 'blob' });
const zipUrl = window.URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = zipUrl;
a.download = 'images.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(zipUrl);
}
};

export default imageZipDownloader;

0 comments on commit 127cfc4

Please sign in to comment.