이정민

Merge branch 'feat/web' into develop

...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "@testing-library/user-event": "^12.1.10", 9 "@testing-library/user-event": "^12.1.10",
10 "@toast-ui/react-image-editor": "3.14.2", 10 "@toast-ui/react-image-editor": "3.14.2",
11 "@types/fabric": "^4.2.5", 11 "@types/fabric": "^4.2.5",
12 + "axios": "^0.21.1",
12 "fabric": "^4.4.0", 13 "fabric": "^4.4.0",
13 "next": "^10.0.5", 14 "next": "^10.0.5",
14 "react": "^17.0.2", 15 "react": "^17.0.2",
......
1 +import axios from "axios";
2 +
3 +const baseURL = "https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com";
4 +
5 +export const postGif = async (formData) => {
6 + console.log("file", formData);
7 + const { data } = await axios.post(baseURL, formData);
8 +
9 + return data;
10 +};
11 +
12 +export const getGif = async (id) => {
13 + const { data } = await axios.get(baseURL, {
14 + params: {
15 + id: id,
16 + },
17 + });
18 +
19 + return data;
20 +};
1 import { useEffect, useRef, useState } from "react"; 1 import { useEffect, useRef, useState } from "react";
2 -import { fabric } from "fabric";
3 import styled from "styled-components"; 2 import styled from "styled-components";
4 -import { GifGenerator } from "gif-generator/src"; 3 +import TuiImageEditor from "tui-image-editor";
5 -import ImageEditor from "@toast-ui/react-image-editor"; 4 +import "gif-generator/dist/gif-generator";
5 +import { postGif } from "api";
6 +
7 +declare global {
8 + interface Window {
9 + GifGenerator: any;
10 + }
11 +}
6 12
7 const GifEditor = ({ previewURL }) => { 13 const GifEditor = ({ previewURL }) => {
8 - const [canvas, setCanvas] = useState<HTMLCanvasElement>(); 14 + const [imageEditor, setImageEditor] = useState(null);
9 - const [gifGenerator, setGifGenerator] = useState(null); 15 +
16 + const rootEl = useRef();
17 +
18 + const [download, setDownload] = useState(null);
19 + const [blob, setBlob] = useState(null);
20 +
21 + const [isMakeStarted, setIsMakeStarted] = useState(false);
22 + const [isUploadLoading, setIsUploadLoading] = useState(false);
23 + const [viewLink, setViewLink] = useState(null);
10 24
11 useEffect(() => { 25 useEffect(() => {
12 if (window) { 26 if (window) {
13 - setCanvas( 27 + setImageEditor(
14 - document.getElementsByClassName( 28 + new TuiImageEditor(rootEl.current, {
15 - "tui-image-editor-container" 29 + includeUI: {
16 - )[0] as HTMLCanvasElement 30 + loadImage: {
31 + path: previewURL,
32 + name: "SampleImage",
33 + },
34 + uiSize: {
35 + width: "100%",
36 + height: "600px",
37 + },
38 + menu: ["draw", "text"],
39 + menuBarPosition: "bottom",
40 + },
41 + cssMaxWidth: 500,
42 + cssMaxHeight: 700,
43 + usageStatistics: false,
44 + })
17 ); 45 );
18 } 46 }
19 }, []); 47 }, []);
20 - console.log(
21 - "asdf",
22 - document.getElementsByClassName("tui-image-editor-container")
23 - );
24 - console.log("canvas", canvas);
25 - console.log('useref',useRef.arguments)
26 -
27 - useEffect(()=>{
28 - if(canvas)setGifGenerator(new GifGenerator(canvas._graphics.getCanvas()));
29 - }, [canvas])
30 48
31 - // useEffect(() => { 49 + useEffect(() => {
32 - // // const img = lowerCanvas?.toDataURL("image/png"); 50 + if (imageEditor) {
33 - // // const uploaded = document.getElementById("image"); 51 + console.log(imageEditor._graphics.getCanvas().getObjects());
34 - // // console.log(uploaded); 52 + }
35 - // // let w = window.open(); 53 + }, [imageEditor]);
36 - // // if (w?.window) w.document.body.innerHTML = "<img src='" + img + "'>";
37 - // const image = new Image();
38 - // // image.onload = function () {
39 - // // lowerCanvas.width = uploaded.clientWidth;
40 - // // lowerCanvas.height = uploaded.clientHeight;
41 - // // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
42 - // // };
43 - // image.src = previewURL;
44 - // console.log("canvascontext", canvas?.getContext);
45 - // if (canvas?.getContext) {
46 - // console.log('왜안돼')
47 - // image.onload = function () {
48 - // canvas.width = 1000;
49 - // canvas.height = 572;
50 - // canvas?.getContext("2d").drawImage(image, 0, 0);
51 - // };
52 - // // console.log(canvas.getContext("2d"));
53 - // }
54 - // }, [canvas]);
55 54
56 - const render = () => { 55 + const makeGif = () => {
56 + setIsMakeStarted(true);
57 + const gifGenerator = new window.GifGenerator(
58 + imageEditor._graphics.getCanvas()
59 + );
57 gifGenerator.make().then( 60 gifGenerator.make().then(
58 (blob) => { 61 (blob) => {
59 - window.open(window.URL.createObjectURL(blob)); 62 + setBlob(blob);
63 + setDownload(window.URL.createObjectURL(blob));
60 }, 64 },
61 (error) => { 65 (error) => {
62 alert(error); 66 alert(error);
...@@ -64,44 +68,58 @@ const GifEditor = ({ previewURL }) => { ...@@ -64,44 +68,58 @@ const GifEditor = ({ previewURL }) => {
64 ); 68 );
65 }; 69 };
66 70
71 + const handleUpload = async () => {
72 + setIsUploadLoading(true);
73 + const file = new File([blob], "new_gif.gif");
74 + const formData = new FormData();
75 + formData.append("gif", file);
76 + const res = await postGif(formData);
77 + console.log(res);
78 + setIsUploadLoading(false);
79 + setViewLink(
80 + `https://gif-generator.s3.ap-northeast-2.amazonaws.com//gif/${res.id}.gif`
81 + );
82 + };
83 +
67 return ( 84 return (
68 - <Container> 85 + <>
69 - <div onClick={render} className="upload"> 86 + <Wrapper>
70 - Save 87 + {((isMakeStarted && !download) || isUploadLoading) && (
88 + <>
89 + <div className="background" />
90 + <div className="download">loading...</div>
91 + </>
92 + )}
93 + {!isUploadLoading && viewLink && (
94 + <div className="download" style={{ zIndex: 200 }}>
95 + <a href={viewLink}>{viewLink}</a>
71 </div> 96 </div>
72 - <ImageEditor 97 + )}
73 - includeUI={{ 98 + {download && !isUploadLoading && (
74 - loadImage: { 99 + <>
75 - path: previewURL, 100 + <div className="background" />
76 - name: "SampleImage", 101 + <div className="download">
77 - }, 102 + <div className="download__btn">
78 - menu: ["draw", "text"], 103 + <a href={download} download="new_gif.gif">
79 - initMenu: "draw", 104 + Download a File
80 - uiSize: { 105 + </a>
81 - width: "100%", 106 + </div>
82 - height: "600px", 107 + <div className="download__btn">
83 - }, 108 + <div onClick={handleUpload}>Upload to Server</div>
84 - menuBarPosition: "bottom", 109 + </div>
85 - }} 110 + </div>
86 - cssMaxHeight={500} 111 + </>
87 - cssMaxWidth={700} 112 + )}
88 - selectionStyle={{ 113 + <div onClick={makeGif} className="make">
89 - cornerSize: 20, 114 + Make a Gif
90 - rotatingPointOffset: 70, 115 + </div>
91 - }} 116 + <div ref={rootEl} />
92 - usageStatistics={true} 117 + </Wrapper>
93 - /> 118 + </>
94 - <div className="alert">Please select a photo.</div>
95 - </Container>
96 - // <Container>
97 - // <ImgBox>
98 - // <canvas id="gif-canvas" />
99 - // </ImgBox>
100 - // </Container>
101 ); 119 );
102 }; 120 };
103 121
104 -const Container = styled.div` 122 +const Wrapper = styled.div`
105 position: fixed; 123 position: fixed;
106 width: 90%; 124 width: 90%;
107 top: 10rem; 125 top: 10rem;
...@@ -110,7 +128,11 @@ const Container = styled.div` ...@@ -110,7 +128,11 @@ const Container = styled.div`
110 display: flex; 128 display: flex;
111 flex-direction: column; 129 flex-direction: column;
112 align-items: center; 130 align-items: center;
113 - .upload { 131 + a {
132 + color: black;
133 + text-decoration: none;
134 + }
135 + .make {
114 font: 800 11.5px Arial; 136 font: 800 11.5px Arial;
115 position: absolute; 137 position: absolute;
116 right: 0; 138 right: 0;
...@@ -127,11 +149,30 @@ const Container = styled.div` ...@@ -127,11 +149,30 @@ const Container = styled.div`
127 justify-content: center; 149 justify-content: center;
128 cursor: pointer; 150 cursor: pointer;
129 } 151 }
130 - .alert { 152 + .background {
131 position: fixed; 153 position: fixed;
132 - border-radius: 0.5rem; 154 + top: 0;
133 - transition: 1s; 155 + left: 0;
134 - top: 7rem; 156 + width: 100%;
157 + height: 100vh;
158 + background-color: black;
159 + opacity: 0.7;
160 + z-index: 100;
161 + }
162 + .download {
163 + position: absolute;
164 + top: 15rem;
165 + z-index: 100;
166 + display: flex;
167 + background-color: white;
168 + padding: 1.5rem 2rem;
169 + border-radius: 2rem;
170 + &__btn {
171 + cursor: pointer;
172 + :last-child {
173 + margin-left: 1rem;
174 + }
175 + }
135 } 176 }
136 .tui-image-editor-container { 177 .tui-image-editor-container {
137 border-radius: 1.5rem; 178 border-radius: 1.5rem;
...@@ -143,6 +184,9 @@ const Container = styled.div` ...@@ -143,6 +184,9 @@ const Container = styled.div`
143 .tui-image-editor-header-logo { 184 .tui-image-editor-header-logo {
144 display: none; 185 display: none;
145 } 186 }
187 + .tui-image-editor-header-buttons {
188 + display: none;
189 + }
146 `; 190 `;
147 191
148 export default GifEditor; 192 export default GifEditor;
......
1 -import dynamic from "next/dynamic";
2 -import { useState } from "react";
3 -import styled from "styled-components";
4 -
5 -const ToastEditor = dynamic(() => import("components/ToastEditor"), {
6 - ssr: false,
7 -});
8 -const Image = ({ previewURL, setPreviewURL }) => {
9 - const [file, setFile] = useState(undefined);
10 - // console.log("previewURL", previewURL);
11 -
12 - // const uploadImage = (file) => {
13 - // if (!file) {
14 - // return;
15 - // }
16 - // };
17 -
18 - // const selectImg = (e) => {
19 - // const reader = new FileReader();
20 - // const targetFile = e.target.files[0];
21 - // setFile(targetFile);
22 - // // uploadImage(targetFile);
23 -
24 - // reader.onloadend = () => {
25 - // setPreviewURL(reader.result);
26 - // };
27 -
28 - // reader.readAsDataURL(targetFile);
29 - // };
30 -
31 - // const [isEditorOpened, setIsEditorOpened] = useState(false);
32 - // const handleEditor = () => {
33 - // setIsEditorOpened(true);
34 - // };
35 -
36 - return (
37 - <>
38 - <Container>
39 - <ImgBox>
40 - {/* <div onClick={handleEditor}>asdf</div> */}
41 - {/* {file === undefined ? ( */}
42 - <>
43 - {/* <div className="sub-flex">
44 - <BlankBox />
45 - <div>Click to add a photo</div>
46 - <input
47 - type="file"
48 - style={{
49 - position: "absolute",
50 - top: 0,
51 - paddingLeft: 0,
52 - zIndex: 0,
53 - width: "90%",
54 - height: "100%",
55 - border: "none",
56 - cursor: "pointer",
57 - outline: "none",
58 - }}
59 - onChange={selectImg}
60 - />
61 - </div>
62 - <div className="sub-flex">Open Image Editor</div> */}
63 - </>
64 - {/* ) : ( */}
65 - <img
66 - id="image"
67 - alt={""}
68 - style={{
69 - objectFit: "cover",
70 - display: "flex",
71 - maxHeight: "90%",
72 - maxWidth: "90%",
73 - }}
74 - src={previewURL as string}
75 - />
76 - {/* )} */}
77 - </ImgBox>
78 - {/* <Menu /> */}
79 - </Container>
80 - {/* {isEditorOpened && <ToastEditor {...{ setPreviewURL, setIsImgAdded }} />} */}
81 - </>
82 - );
83 -};
84 -
85 -const Menu = () => {
86 - return (
87 - <div style={{ width: "15rem", marginLeft: "2rem" }}>
88 - <Box />
89 - <Box />
90 - <Box />
91 - <Box />
92 - </div>
93 - );
94 -};
95 -
96 -const Container = styled.div`
97 - width: 100%;
98 - display: flex;
99 - justify-content: center;
100 - margin-top: 10rem;
101 -`;
102 -const ImgBox = styled.div`
103 - position: relative;
104 - width: 90%;
105 - /* height: 30rem; */
106 - background-color: white;
107 - box-shadow: ${({ theme }) => theme.boxShadow.normal};
108 - border-radius: 2rem;
109 - margin-top: 2rem;
110 - display: flex;
111 - align-items: center;
112 - justify-content: center;
113 - font-size: 1rem;
114 - display: flex;
115 - /* flex: 0.6; */
116 - padding: 1rem 0;
117 - /* .sub-flex {
118 - position: relative;
119 - width: 100%;
120 - height: 100%;
121 - display: flex;
122 - align-items: center;
123 - justify-content: center;
124 - :first-child {
125 - border-right: 1px solid ${({ theme }) => theme.color.gray};
126 - }
127 - } */
128 -`;
129 -
130 -const Box = styled.div`
131 - width: 100%;
132 - height: 10rem;
133 - margin-top: 2rem;
134 - border-radius: 1rem;
135 - background-color: white;
136 - box-shadow: ${({ theme }) => theme.boxShadow.normal};
137 -`;
138 -
139 -
140 -export default Image;
1 /// <reference path="react-image-editor.d.ts" /> 1 /// <reference path="react-image-editor.d.ts" />
2 import ImageEditor from "@toast-ui/react-image-editor"; 2 import ImageEditor from "@toast-ui/react-image-editor";
3 -import { useEffect, useState } from "react"; 3 +import { useState } from "react";
4 import styled from "styled-components"; 4 import styled from "styled-components";
5 import "tui-image-editor/dist/tui-image-editor.css"; 5 import "tui-image-editor/dist/tui-image-editor.css";
6 -import { GifGenerator } from "gif-generator/src/index";
7 6
8 const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => { 7 const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
9 - // const [lowerCanvas, setLowerCanvas] = useState<HTMLCanvasElement>();
10 - // const [upperCanvas, setUpperCanvas] = useState<HTMLCanvasElement>();
11 - // // console.log(
12 - // // document.getElementsByClassName("lower-canvas")[0]?.toDataURL("image/png")
13 - // // );
14 - // console.log("s");
15 -
16 - // // const [upperCanvas, setUpperCanvas] = useState(
17 - // // document.getElementsByClassName("upper-canvas ")[0]
18 - // // );
19 -
20 - // useEffect(() => {
21 - // window?.addEventListener("click", () => {
22 - // setLowerCanvas(
23 - // document.getElementsByClassName("lower-canvas")[0] as HTMLCanvasElement
24 - // );
25 - // setUpperCanvas(
26 - // document.getElementsByClassName("upper-canvas")[0] as HTMLCanvasElement
27 - // );
28 - // });
29 - // }, []);
30 -
31 - // useEffect(() => {
32 - // const img = lowerCanvas?.toDataURL("image/png");
33 - // const uploaded = document.getElementById("image");
34 - // console.log(uploaded);
35 - // // let w = window.open();
36 - // // if (w?.window) w.document.body.innerHTML = "<img src='" + img + "'>";
37 - // const image = new Image();
38 - // // image.onload = function () {
39 - // // lowerCanvas.width = uploaded.clientWidth;
40 - // // lowerCanvas.height = uploaded.clientHeight;
41 - // // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
42 - // // };
43 - // image.src = previewURL;
44 - // console.log("b");
45 - // if (lowerCanvas?.getContext&&upperCanvas?.getContext) {
46 - // image.onload = function () {
47 -
48 - // lowerCanvas.width = 1000;
49 - // lowerCanvas.height = 572;
50 - // upperCanvas.width = 1000;
51 - // upperCanvas.height = 572;
52 - // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
53 - // };
54 - // console.log(lowerCanvas.getContext("2d"));
55 - // }
56 - // }, [lowerCanvas?.toDataURL("image/png")]);
57 -
58 const [alertIsShown, setAlertIsShown] = useState(false); 8 const [alertIsShown, setAlertIsShown] = useState(false);
59 9
60 const handleEnd = () => { 10 const handleEnd = () => {
...@@ -76,43 +26,16 @@ const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => { ...@@ -76,43 +26,16 @@ const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
76 } 26 }
77 }; 27 };
78 28
79 - // console.log('asdf',document
80 - // .getElementsByClassName("tui-image-editor-container"))
81 -
82 - // window.GifGenerator = GifGenerator;
83 -
84 - // let gifGenerator;
85 - // setTimeout(function () {
86 - // gifGenerator = new GifGenerator(
87 - // document
88 - // .getElementsByClassName("tui-image-editor-container")
89 - // ._graphics.getCanvas()
90 - // );
91 - // }, 1000);
92 - // function render() {
93 - // gifGenerator.make().then(
94 - // (blob) => {
95 - // window.open(window.URL.createObjectURL(blob));
96 - // },
97 - // (error) => {
98 - // alert(error);
99 - // }
100 - // );
101 - // }
102 -
103 return ( 29 return (
104 <Container> 30 <Container>
105 - <div onClick={handleEnd} className="upload"> 31 + <div onClick={handleEnd} className="move">
106 - Upload 32 + Move to Gif
107 </div> 33 </div>
108 <ImageEditor 34 <ImageEditor
109 includeUI={{ 35 includeUI={{
110 loadImage: { 36 loadImage: {
111 - // path: 'img/sampleImage.jpg',
112 name: "SampleImage", 37 name: "SampleImage",
113 }, 38 },
114 - // theme: myTheme,
115 - initMenu: "filter",
116 uiSize: { 39 uiSize: {
117 width: "100%", 40 width: "100%",
118 height: "600px", 41 height: "600px",
...@@ -143,7 +66,7 @@ const Container = styled.div` ...@@ -143,7 +66,7 @@ const Container = styled.div`
143 display: flex; 66 display: flex;
144 flex-direction: column; 67 flex-direction: column;
145 align-items: center; 68 align-items: center;
146 - .upload { 69 + .move {
147 font: 800 11.5px Arial; 70 font: 800 11.5px Arial;
148 position: absolute; 71 position: absolute;
149 right: 0; 72 right: 0;
......
...@@ -51,7 +51,7 @@ export class GifGenerator { ...@@ -51,7 +51,7 @@ export class GifGenerator {
51 const objs = []; 51 const objs = [];
52 52
53 fabricObjs.map((fabricObj) => { 53 fabricObjs.map((fabricObj) => {
54 - if (fabricObj instanceof fabric.Path) { 54 + if (fabricObj.path !== undefined) {
55 objs.push(new Component.Brush(fabricObj)); 55 objs.push(new Component.Brush(fabricObj));
56 this.canvas.remove(fabricObj); 56 this.canvas.remove(fabricObj);
57 } else if (fabricObj.text !== undefined) { 57 } else if (fabricObj.text !== undefined) {
......
1 +import { useRouter } from "next/dist/client/router";
2 +
3 +const Detail = () => {
4 + const id = useRouter().query.id;
5 +
6 + return (
7 + <div>
8 + <img
9 + src={`https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com/?id=${id}`}
10 + />
11 + </div>
12 + );
13 +};
14 +
15 +export default Detail;
1 import Header from "components/Header"; 1 import Header from "components/Header";
2 -import Image from "components/Image";
3 import styled from "styled-components"; 2 import styled from "styled-components";
4 import dynamic from "next/dynamic"; 3 import dynamic from "next/dynamic";
5 import { useState } from "react"; 4 import { useState } from "react";
......
...@@ -1856,6 +1856,13 @@ axe-core@^4.0.2: ...@@ -1856,6 +1856,13 @@ axe-core@^4.0.2:
1856 resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224" 1856 resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
1857 integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg== 1857 integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
1858 1858
1859 +axios@^0.21.1:
1860 + version "0.21.1"
1861 + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
1862 + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
1863 + dependencies:
1864 + follow-redirects "^1.10.0"
1865 +
1859 axobject-query@^2.2.0: 1866 axobject-query@^2.2.0:
1860 version "2.2.0" 1867 version "2.2.0"
1861 resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" 1868 resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
...@@ -3184,6 +3191,11 @@ flatted@^3.1.0: ...@@ -3184,6 +3191,11 @@ flatted@^3.1.0:
3184 resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" 3191 resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
3185 integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== 3192 integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
3186 3193
3194 +follow-redirects@^1.10.0:
3195 + version "1.14.1"
3196 + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
3197 + integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
3198 +
3187 foreach@^2.0.5: 3199 foreach@^2.0.5:
3188 version "2.0.5" 3200 version "2.0.5"
3189 resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 3201 resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
......