깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)
참고
- 깃허브 리포지토리 이미지 불러오기
- 깃허브 API로 이미지 업로드하기
- 깃허브에 업로드된 이미지 삭제하기
- 캡처한 이미지를 깃허브에 업로드하기
- 캡처한 이미지 여러 개 업로드하기
- Toast UI 에디터로 이미지를 포함한 깃허브 마크다운 저장하기
아래와 같이 깃허브 API를 이용하여 이미지 파일을 업로드하고, 다시 이미지를 잘 불러오는지 확인해 보자.
테스트하기 편하게 이전 글의 코드를 유지하고 깃허브 리포지토리에서 이미지 파일 하나만 남겨두었다.
ReactImageList.js
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import ImageList from "@mui/material/ImageList";
import ImageListItem from "@mui/material/ImageListItem";
import ImageListItemBar from "@mui/material/ImageListItemBar";
import ListSubheader from "@mui/material/ListSubheader";
import IconButton from "@mui/material/IconButton";
import InfoIcon from "@mui/icons-material/Info";
import * as gh from "../githublibrary.js";
const ReactImageList = () => {
const [itemData, setItemData] = useState([]);
const fileLoad = async () => {
let result = await gh.fileRead("images");
let fileList = result.data.map((item) => item.path);
let temp = [];
let count = 1;
for (let path of fileList) {
let obj = {
img: `https://github.com/bloodstrawberry/auto-test/raw/main/${path}`,
title: `image_${count++}`,
author: "bloodstrawberry",
};
temp.push(obj);
}
setItemData(temp);
};
useEffect(() => {
fileLoad();
}, []);
return (
<Box sx={{ m: 3 }}>
<ImageList sx={{ width: 248 * 3 }} cols={3}>
<ImageListItem key="Subheader" cols={3}>
<ListSubheader component="div">GitHub Images Directory</ListSubheader>
</ImageListItem>
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
src={`${item.img}?w=248&fit=crop&auto=format`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: "rgba(255, 255, 255, 0.54)" }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
</ImageListItem>
))}
</ImageList>
</Box>
);
};
export default ReactImageList;
파일 업로드 버튼 추가하기
업로드 버튼은 다음과 같다.
// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
const ReactImageList = () => {
...
<Button
component="label"
variant="contained"
startIcon={<CloudUploadIcon />}
onChange={handleFileUpload}
>
Upload file
<VisuallyHiddenInput type="file" accept="image/*" multiple />
</Button>
파일 업로드는 input 태그에 type="file"을 설정하면 된다.
accept로 업로드 가능한 파일을 설정할 수 있고, multiple로 여러 파일을 선택하도록 할 수 있다.
만약 jpeg과 png 확장자를 하나만 업로드하고 싶다면 아래와 같이 설정하면 된다.
<VisuallyHiddenInput type="file" accept="image/jpeg, image/png" />
이제 handleFileUpload를 구현해 보자.
handleFileUpload 구현
파일을 업로드하면 onChange에 업로드한 파일들의 정보를 알 수 있다. (target.files)
파일을 여러 개 업로드한다면 각 파일마다 github에 업로드하면 된다.
const handleFileUpload = (e) => {
const selectedImages = e.target.files;
for (let selectedImage of selectedImages) {
githubUpload(selectedImage);
}
};
githubUpload는 다음과 같다.
RESTful API로 이미지를 그대로 업로드(file write)하는 것은 불가능하다.
base64로 encoding 한 후 아래 API를 이용하면 이미지를 업로드할 수 있다.
const githubUpload = (image) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64encoded = reader.result.split(",")[1];
const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
const accessToken = process.env.REACT_APP_MY_TOKEN;
fetch(apiURL, {
method: "PUT",
headers: {
Authorization: `token ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Add image",
content: base64encoded,
branch: "main",
}),
})
.then((response) => response.json())
.then((data) => {
console.log("Image uploaded successfully:", data.content.name);
})
.catch((error) => {
console.error("Error uploading image:", error);
});
};
reader.readAsDataURL(image);
};
axios 버전은 다음과 같다.
import axios from "axios";
const githubUpload = async (image) => {
const reader = new FileReader();
reader.onloadend = async () => {
const base64encoded = reader.result.split(",")[1];
const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
const accessToken = process.env.REACT_APP_MY_TOKEN;
try {
const response = await axios.put(
apiURL,
{
message: "Add image",
content: base64encoded,
branch: "main",
},
{
headers: {
Authorization: `token ${accessToken}`,
"Content-Type": "application/json",
},
}
);
console.log("Image uploaded successfully:", response.data.content.name);
} catch (error) {
console.error("Error uploading image:", error);
}
};
reader.readAsDataURL(image);
};
const handleFileUpload = async (e) => {
const selectedImages = e.target.files;
for (let selectedImage of selectedImages) {
await githubUpload(selectedImage);
}
};
참고로 apiURL은 EnterPrise ver의 경우 BASE URL을 설정해야 한다.
const apiURL = '[BASE URL]/api/v3/repos/YOUR_USERNAME/YOUR_REPO_NAME/contents/{path or fileName}';
충돌 해결
위의 코드를 토대로 여러 파일을 업로드해 보자.
아쉽게도 여러 파일을 선택해서 업로드하면 conflict이 발생(409 Error)한다.
아래 로그는 5개 파일을 업로드하였을 때, 2개의 파일만 성공한 것을 보여준다.
이 에러를 해결하기 위해, 업로드할 때마다 delay를 주자.
여러 이미지가 있는 경우 5초마다 Upload 하도록 Promise를 추가하였다.
const handleFileUpload = (e) => {
const selectedImages = e.target.files;
const uploadImageWithDelay = async () => {
githubUpload(selectedImages[0]);
for (let i = 1; i < selectedImages.length; i++) {
await new Promise((resolve) => {
setTimeout(() => {
githubUpload(selectedImages[i]);
resolve();
}, 5000); // 5초 지연
});
}
};
uploadImageWithDelay();
};
또는 서버를 이용하여 파일들을 한 번에 node js의 multer로 업로드하고 서버에서 git push를 할 수도 있다.
전체 코드는 다음과 같다.
ReactImageList.js
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import ImageList from "@mui/material/ImageList";
import ImageListItem from "@mui/material/ImageListItem";
import ImageListItemBar from "@mui/material/ImageListItemBar";
import ListSubheader from "@mui/material/ListSubheader";
import IconButton from "@mui/material/IconButton";
import InfoIcon from "@mui/icons-material/Info";
// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import axios from "axios";
import * as gh from "../githublibrary.js";
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
const ReactImageList = () => {
const [itemData, setItemData] = useState([]);
const fileLoad = async () => {
let result = await gh.fileRead("images");
let fileList = result.data.map((item) => item.path);
let temp = [];
let count = 1;
for (let path of fileList) {
let obj = {
img: `https://github.com/bloodstrawberry/auto-test/raw/main/${path}`,
title: `image_${count++}`,
author: "bloodstrawberry",
};
temp.push(obj);
}
setItemData(temp);
};
useEffect(() => {
fileLoad();
}, []);
// const githubUpload = (image) => {
// const reader = new FileReader();
// reader.onloadend = () => {
// const base64encoded = reader.result.split(",")[1];
// const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
// const accessToken = process.env.REACT_APP_MY_TOKEN;
// fetch(apiURL, {
// method: "PUT",
// headers: {
// Authorization: `token ${accessToken}`,
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// message: "Add image",
// content: base64encoded,
// branch: "main",
// }),
// })
// .then((response) => response.json())
// .then((data) => {
// console.log("Image uploaded successfully:", data.content.name);
// })
// .catch((error) => {
// console.error("Error uploading image:", error);
// });
// };
// reader.readAsDataURL(image);
// };
const githubUpload = async (image) => {
const reader = new FileReader();
reader.onloadend = async () => {
const base64encoded = reader.result.split(",")[1];
const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
const accessToken = process.env.REACT_APP_MY_TOKEN;
try {
const response = await axios.put(
apiURL,
{
message: "Add image",
content: base64encoded,
branch: "main",
},
{
headers: {
Authorization: `token ${accessToken}`,
"Content-Type": "application/json",
},
}
);
console.log("Image uploaded successfully:", response.data.content.name);
} catch (error) {
console.error("Error uploading image:", error);
}
};
reader.readAsDataURL(image);
};
const handleFileUpload = (e) => {
const selectedImages = e.target.files;
const uploadImageWithDelay = async () => {
githubUpload(selectedImages[0]);
for (let i = 1; i < selectedImages.length; i++) {
await new Promise((resolve) => {
setTimeout(() => {
githubUpload(selectedImages[i]);
resolve();
}, 5000); // 5초 delay
});
}
};
uploadImageWithDelay();
};
return (
<Box sx={{ m: 3 }}>
<Button
component="label"
variant="contained"
startIcon={<CloudUploadIcon />}
onChange={handleFileUpload}
>
Upload file
<VisuallyHiddenInput type="file" accept="image/*" multiple />
</Button>
<ImageList sx={{ width: 248 * 3 }} cols={3}>
<ImageListItem key="Subheader" cols={3}>
<ListSubheader component="div">GitHub Images Directory</ListSubheader>
</ImageListItem>
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
src={`${item.img}?w=248&fit=crop&auto=format`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: "rgba(255, 255, 255, 0.54)" }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
</ImageListItem>
))}
</ImageList>
</Box>
);
};
export default ReactImageList;
githublibrary.js
import axios from "axios";
import { Octokit } from "@octokit/rest";
const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`;
export const fileRead = async (path) => {
try {
const octokit = new Octokit({
auth: myKey,
});
const result = await octokit.request(
`GET /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
encoding: "utf-8",
decoding: "utf-8",
}
);
return result;
} catch (e) {
console.log("error : ", e);
return undefined;
}
};
댓글