diff --git a/package-lock.json b/package-lock.json
index 56071c4..a7558dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,11 +11,14 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.4.0",
"dotting": "^1.0.13",
"react": "^18.2.0",
+ "react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.1",
"react-scripts": "5.0.1",
+ "styled-components": "^5.3.11",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -2142,6 +2145,29 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+ "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -3821,6 +3847,11 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
+ "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
+ },
"node_modules/@types/eslint": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz",
@@ -3874,6 +3905,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -5097,6 +5137,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -5320,6 +5383,26 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/babel-plugin-styled-components": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.3.tgz",
+ "integrity": "sha512-jBioLwBVHpOMU4NsueH/ADcHrjS0Y/WTpt2eGVmmuSFNEv2DF3XhcMncuZlbbjxQ4vzxg+yEr6E6TNjrIQbsJQ==",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-module-imports": "^7.21.4",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.21",
+ "picomatch": "^2.3.1"
+ },
+ "peerDependencies": {
+ "styled-components": ">= 2"
+ }
+ },
+ "node_modules/babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw=="
+ },
"node_modules/babel-plugin-transform-react-remove-prop-types": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
@@ -5633,6 +5716,14 @@
"node": ">= 6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -6068,6 +6159,14 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-declaration-sorter": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
@@ -6249,6 +6348,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -8712,6 +8821,19 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -14088,6 +14210,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -14232,6 +14359,19 @@
"node": ">=14"
}
},
+ "node_modules/react-cookie": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
+ "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.0.1",
+ "hoist-non-react-statics": "^3.0.0",
+ "universal-cookie": "^4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0"
+ }
+ },
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -15204,6 +15344,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -15606,6 +15751,35 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
+ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0",
+ "react-is": ">= 16.8.0"
+ }
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -16276,6 +16450,23 @@
"node": ">=8"
}
},
+ "node_modules/universal-cookie": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
+ "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
+ "dependencies": {
+ "@types/cookie": "^0.3.3",
+ "cookie": "^0.4.0"
+ }
+ },
+ "node_modules/universal-cookie/node_modules/cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
diff --git a/package.json b/package.json
index ec5e5a9..fabdeef 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,14 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.4.0",
"dotting": "^1.0.13",
"react": "^18.2.0",
+ "react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.1",
"react-scripts": "5.0.1",
+ "styled-components": "^5.3.11",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/src/App.js b/src/App.js
index 768e834..719ea4b 100644
--- a/src/App.js
+++ b/src/App.js
@@ -8,6 +8,7 @@ import PostEditPage from "./routes/PostEditPage";
import SignUpPage from "./routes/SignUpPage";
import PostDetailPage from "./routes/PostDetailPage";
import SignInPage from "./routes/SignInPage";
+import MyInfoPage from "./routes/MyInfoPage";
function App() {
return (
@@ -25,8 +26,10 @@ function App() {
} />
{/* sign up */}
} />
- {/* sign up */}
+ {/* sign in */}
} />
+ {/* My Info */}
+ } />
diff --git a/src/apis/api.js b/src/apis/api.js
new file mode 100644
index 0000000..0b6abe2
--- /dev/null
+++ b/src/apis/api.js
@@ -0,0 +1,197 @@
+import { removeCookie } from "../utils/cookie";
+import { instance, instanceWithToken } from "./axios";
+
+
+// src/api/api.js
+// Account 관련 API들
+export const signIn = async (data) => {
+ const response = await instance.post("/account/signin/", data);
+ if (response.status === 200) {
+ window.location.href = "/";
+ } else {
+ console.log("Error");
+ }
+};
+
+export const signUp = async (data) => {
+ const response = await instance.post("/account/signup/", data);
+ if (response.status === 200) {
+ window.location.href = "/"; //새로고침 있음
+ }
+ return response;
+};
+
+// 추가!!
+
+export const logOut = async (token) => {
+ const response = await instanceWithToken.post("/account/logout/", {
+ refresh: token,
+ });
+ if (response.status === 204) {
+ console.log("REFRESH TOKEN SUCCESS");
+
+ removeCookie("refresh_token");
+ removeCookie("access_token");
+
+ window.location.reload();
+ } else {
+ console.log("[ERROR] error while refreshing token");
+ }
+};
+
+// 추가!!
+
+
+//Post 관련 API들
+export const getPosts = async () => {
+ const response = await instance.get("/post/");
+ return response.data;
+};
+
+export const getPost = async (id) => {
+ const response = await instance.get(`/post/${id}/`);
+ return response.data;
+};
+
+export const createPost = async (data, navigate) => {
+ const response = await instanceWithToken.post("/post/", data);
+ if (response.status === 201) {
+ console.log("POST SUCCESS");
+ navigate("/");
+ } else {
+ console.log("[ERROR] error while creating post");
+ }
+};
+
+export const updatePost = async (id, data, navigate) => {
+ const response = await instanceWithToken.patch(`/post/${id}/`, data);
+ if (response.status === 200) {
+ console.log("POST UPDATE SUCCESS");
+ navigate(-1);
+ } else {
+ console.log("[ERROR] error while updating post");
+ }
+};
+
+
+// 과제!!
+export const deletePost = async (id) => {
+ const response = await instanceWithToken.delete(`/post/${id}/`);
+ if(response.status===204){
+ console.log("POST DELETE SUCCESS");
+ window.location.href = "/";
+ }else{
+ console.log("[ERROR] error while deleting post");
+ }
+
+};
+
+// 과제!!
+export const likePost = async (postId) => {
+ const response = await instanceWithToken.post(`/post/${postId}/like/`,postId);
+ if(response.status===200)
+ console.log("LIKE UPDATE SUCCESS");
+ else if(response.status===401)
+ alert("로그인 후 시도해주세요");
+ else
+ console.log("[ERROR] error while editing like");
+ // console.log(response.data['like_users'].length);
+ //return like count
+ return response.data['like_users'].length;
+};
+
+
+// Tag 관련 API들
+export const getTags = async () => {
+ const response = await instance.get("/tag/");
+ return response.data;
+};
+
+export const createTag = async (data) => {
+ const response = await instanceWithToken.post("/tag/", data);
+ if (response.status === 201) {
+ console.log("TAG SUCCESS");
+ } else {
+ console.log("[ERROR] error while creating tag");
+ }
+ return response; // response 받아서 그 다음 처리
+};
+
+
+// Comment 관련 API들
+export const getComments = async (postId) => {
+ const response = await instance.get(`/comment/?post=${postId}`);
+ return response.data;
+};
+
+export const createComment = async (data) => {
+ const response = await instanceWithToken.post("/comment/", data);
+ if (response.status === 201) {
+ console.log("COMMENT SUCCESS");
+ window.location.reload(); // 새로운 코멘트 생성시 새로고침으로 반영
+ } else {
+ console.log("[ERROR] error while creating comment");
+ }
+};
+
+export const updateComment = async (id, data) => {
+ const response = await instanceWithToken.patch(`/comment/${id}/`, data);
+ if (response.status === 200) {
+ console.log("COMMENT UPDATE SUCCESS");
+ window.location.reload();
+ } else {
+ console.log("[ERROR] error while updating comment");
+ }
+};
+
+export const getUser =async ()=>{
+ const response = await instanceWithToken.get("/account/info/");
+ if(response.status === 200){
+ console.log("GET USER SUCCESS");
+ }else{
+ console.log("[ERROR] error while updating comment");
+ }
+ return response.data;
+}
+
+export const getUserProfile = async () => {
+ const response = await instanceWithToken.get("/account/profile/");
+ if(response.status === 200){
+ console.log("GET USER SUCCESS");
+ }else{
+ console.log("[ERROR] error while updating comment");
+ }
+ return response.data;
+}
+
+export const updateUserProfile = async (data) => {
+ const response = await instanceWithToken.patch("/account/profile/",data);
+ if(response.status === 200){
+ alert("수정되었습니다.");
+ console.log("UPDATE USER SUCCESS");
+ }else{
+ console.log("[ERROR] error while updating comment");
+ }
+}
+
+
+// 과제 !!
+export const deleteComment = async (id) => {
+ const response = await instanceWithToken.delete(`/comment/${id}/`);
+ if(response.status===204){
+ console.log("COMMENT DELETE SUCCESS");
+ window.location.reload();
+ }else{
+ console.log("[ERROR] error while deleting comment");
+ }
+};
+
+// 추가!!
+export const refreshToken = async (token) => {
+ const response = await instance.post("/account/refresh/", { refresh: token });
+ if (response.status === 200) {
+ console.log("REFRESH TOKEN SUCCESS");
+ } else {
+ console.log("[ERROR] error while refreshing token");
+ }
+};
\ No newline at end of file
diff --git a/src/apis/axios.js b/src/apis/axios.js
new file mode 100644
index 0000000..e31204b
--- /dev/null
+++ b/src/apis/axios.js
@@ -0,0 +1,64 @@
+// src/apis/axios.js
+
+import axios from "axios";
+import { getCookie } from "../utils/cookie";
+import { refreshToken } from "./api";
+
+// baseURL, credential, 헤더 세팅
+axios.defaults.baseURL = 'http://localhost:8000/api';
+axios.defaults.withCredentials = true;
+axios.defaults.headers.post['Content-Type'] = 'application/json';
+axios.defaults.headers.common['X-CSRFToken'] = getCookie('csrftoken');
+
+
+// 누구나 접근 가능한 API들
+export const instance = axios.create();
+
+// Token 있어야 접근 가능한 API들 - 얘는 토큰을 넣어줘야 해요
+export const instanceWithToken = axios.create();
+
+
+// ⬇️ 추가
+// instanceWithToken에는 쿠키에서 토큰을 찾고 담아줍시다!
+instanceWithToken.interceptors.request.use(
+ // 요청을 보내기전 수행할 일
+ // 사실상 이번 세미나에 사용할 부분은 이거밖에 없어요
+ (config) => {
+ const accessToken = getCookie('access_token');
+
+ if (!accessToken) {
+ // token 없으면 리턴
+ return;
+ } else {
+ // token 있으면 헤더에 담아주기 (Authorization은 장고에서 JWT 토큰을 인식하는 헤더 key)
+ config.headers["Authorization"] = `Bearer ${accessToken}`;
+ }
+ return config;
+ },
+
+ // 클라이언트 요청 오류 났을 때 처리
+ (error) => {
+ // 콘솔에 찍어주고, 요청을 보내지 않고 오류를 발생시킴
+ console.log("Request Error!!");
+ return Promise.reject(error);
+ }
+);
+
+instanceWithToken.interceptors.response.use(
+ (response) => {
+ console.log("Interceptor Response!!");
+ return response;
+ },
+ async (error) => {
+ console.log("Response Error!!");
+
+ const originalRequest = error.config;
+ if (error.response.status === 401) { //토큰이 만료됨에 따른 에러인지 확인
+ const token = getCookie("refresh_token");
+ await refreshToken(token); //refresh token 을 활용하여 access token 을 refresh
+
+ return instanceWithToken(originalRequest); //refresh된 access token 을 활용하여 재요청 보내기
+ }
+ return Promise.reject(error);
+ }
+);
\ No newline at end of file
diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx
new file mode 100644
index 0000000..ca35d7a
--- /dev/null
+++ b/src/components/Comment/CommentElement.jsx
@@ -0,0 +1,87 @@
+import { useEffect, useState } from "react";
+import { getUser, updateComment } from "../../apis/api";
+import { getCookie } from "../../utils/cookie";
+
+const CommentElement = (props) => {
+ const { comment, handleCommentDelete } = props;
+ const [content, setContent] = useState(comment.content);
+ const [isEdit, setIsEdit] = useState(false);
+
+ const [user, setUser] = useState(null);
+
+ const date = new Date(comment.created_at);
+ const year = date.getFullYear();
+ let month = date.getMonth() + 1;
+ month = month < 10 ? `0${month}` : month;
+ let day = date.getDate();
+ day = day < 10 ? `0${day}` : day;
+
+ useEffect(() => {
+ if (getCookie("access_token")) {
+ const getUserAPI = async () => {
+ const user = await getUser();
+ setUser(user);
+ };
+ getUserAPI();
+ }
+ }, []);
+
+ // 추가
+ const handleEditComment = () => {
+ updateComment(comment.id, { content: content });
+ };
+ // updateComment 활용
+
+
+ return (
+
+
+ {isEdit ? (
+
setContent(e.target.value)}
+ />
+ ) : (
+
{comment.content}
+ )}
+
+ {year}.{month}.{day}
+
+
+ {/* 수정 */}
+
+ {user?.id === comment.author?.id ? (
+
+ {isEdit ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ ) : null}
+
+ {/* 수정 */}
+
+ );
+};
+export default CommentElement;
\ No newline at end of file
diff --git a/src/components/Comment/index.jsx b/src/components/Comment/index.jsx
index a713ce7..584370b 100644
--- a/src/components/Comment/index.jsx
+++ b/src/components/Comment/index.jsx
@@ -1,24 +1,61 @@
-import { useState } from "react";
-import comments from "../../data/comments";
+import { useEffect, useState } from "react";
import CommentElement from "./CommentElement";
+import { CommentForm } from "../Form";
+import { deleteComment, getComments } from "../../apis/api";
-const Comment = () => {
+export const Comment = ({postId}) => {
// TODO 1: 가짜 comments 불러와서 관리해야겟즤
+ const [commentList, setCommentList] = useState([]);
- // TODO 2: comment추가하는 input 관리해줘야겟지
+ //get comments
+ useEffect(() => {
+ const getCommentAPI=async ()=>{
+ const comments=await getComments(postId);
+ setCommentList(comments);
+ };
+ getCommentAPI();
+ },[postId]);
+
+
// TODO 3: comment Form 제출됐을때 실행되는 함수 만들어줘
- // TODO 4: commet Delete 하는 함수 만들어죠
+
+ // //comment editing
+ // const handleCommentEdit = (commentId, contentEdited) => {
+ // //commentId를 가지고 comment를 찾아서 content를 수정
+ // const newCommentList = commentList.map((comment) => {
+ // if(comment.id === commentId) {
+ // comment.content = contentEdited;
+ // comment.created_at = new Date();
+ // }
+ // return comment;
+ // });
+ // setCommentList(newCommentList);
+ // }
+
+ // TODO 4: commet Delete 하는 함수 만들어죠\
+ const handleCommentDelete = (commentId) => {
+ //commentId를 가지고 comment를 찾아서 삭제
+ if(window.confirm("정말 삭제?")){
+ deleteComment(commentId);
+
+ }
+ }
+
return (
Comments
- // commentElement
- // 가 comment마다 반복시켜야즤
+ {commentList && commentList.map((comment) => (
+
+ ))}
+ {/* // 가 comment마다 반복시켜야즤 */}
-
+
);
};
diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx
index 31aa8a4..55e86e2 100644
--- a/src/components/Form/index.jsx
+++ b/src/components/Form/index.jsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { createComment } from "../../apis/api";
//signup form component
export const SignUpForm = ({formData, setFormData, handleSignUpSubmit})=> {
@@ -260,4 +261,32 @@ export const PostForm = ({ onSubmit, tags, formData, setFormData }) => {
);
- };
\ No newline at end of file
+ };
+
+export const CommentForm = ({postId}) => {
+ const [newContent, setNewContent] = useState("");
+
+ const handleChange=(e)=>{
+ setNewContent(e.target.value);
+ }
+ const onClickSubmit = (e) => {
+ e.preventDefault();
+ createComment({ post: postId, content: newContent });
+ setNewContent("");
+ // postId, newContent만 하나의 객체로 만들어 보내줌
+ };
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx
index 5f0a8c7..600c2fc 100644
--- a/src/components/Header/index.jsx
+++ b/src/components/Header/index.jsx
@@ -1,20 +1,43 @@
import {Link} from "react-router-dom";
import lion from "../../assets/images/lion.jpeg";
+import { getCookie,removeCookie } from "../../utils/cookie";
+import { useEffect, useState } from "react";
+import { logOut } from "../../apis/api";
const Header = () => {
+
+ const [isLoggedIn, setIsLoggedIn] = useState("");
+ useEffect(() => {
+ const loggedIn=getCookie("access_token")?true:false;
+ setIsLoggedIn(loggedIn);
+ }, []);
+
+ const handleLogout = () => {
+ const token = getCookie("refresh_token");
+ logOut(token);
+ };
+
+
return (
);
diff --git a/src/components/ItemEdit/index.jsx b/src/components/ItemEdit/index.jsx
new file mode 100644
index 0000000..753b390
--- /dev/null
+++ b/src/components/ItemEdit/index.jsx
@@ -0,0 +1,55 @@
+import { useState } from "react";
+
+export const EditableInputField = ({
+ content,
+ setContent,
+ handleEdited,
+ label,
+ placeholder,
+}) => {
+ const [edit, setEdit] = useState(false);
+
+ return (
+
+ {label &&
}
+
+ setContent(e.target.value)}
+ disabled={!edit}
+ />
+ {!edit ? (
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Posts/CommentElement.jsx b/src/components/Posts/CommentElement.jsx
deleted file mode 100644
index 2f1cf0d..0000000
--- a/src/components/Posts/CommentElement.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-const CommentElement = (props) => {
- // TODO : props 받기
- // TODO : 수정하는 input 내용 관리
-
- // comment created_at 전처리
- const date = new Date(comment.created_at);
- const year = date.getFullYear();
- let month = date.getMonth() + 1;
- month = month < 10 ? `0${month}` : month;
- let day = date.getDate();
- day = day < 10 ? `0${day}` : day;
-
- return (
-
-
- // 수정중일때와 아닐때를 다르게 보여줘야겠지
- {수정중 ?
:
{내용}
}
- // 날짜
-
- {year}.{month}.{day}
-
- // 수정, 삭제버튼
-
- // delete 버튼은 수정이 아닐때만 보이게 해줘
-
-
-
- );
-};
-
-export default CommentElement;
diff --git a/src/components/Posts/index.jsx b/src/components/Posts/index.jsx
index dd41f79..a18750a 100644
--- a/src/components/Posts/index.jsx
+++ b/src/components/Posts/index.jsx
@@ -1,44 +1,45 @@
-import {Link} from "react-router-dom";
+import { Link } from "react-router-dom";
+import { likePost } from "../../apis/api";
+import { useState } from "react";
export const SmallPost = ({ post }) => {
- const onClickLike = () => {
- console.log("나도 좋아!");
+ const [likeCount, setLikeCount] = useState(post.like_users.length);
+ const onClickLike = async () => {
// add api call for liking post here
+ setLikeCount(await likePost(post.id));
};
return (
{post.title}
{post.author.username}
-
+
{post.tags.map((tag) => (
#{tag.content}
))}
-
- {post.like_users.length > 0 && `❤️ ${post.like_users.length}`}
+
+ ❤️ {likeCount}
-
-
+
+
+ detail
-
-
-
- detail
-
-
-
+
);
};
-
export const BigPost = ({ post }) => {
- const onClickLike = () => {
- console.log("나도 좋아!");
+ const [likeCount, setLikeCount] = useState(post.like_users.length);
+ const onClickLike = async () => {
// add api call for liking post here
+ setLikeCount(await likePost(post.id));
};
return (
@@ -55,10 +56,10 @@ export const BigPost = ({ post }) => {
))}
-
- ❤️ {post.like_users.length > 0 && `${post.like_users.length}`}
+
+ ❤️ {likeCount}
);
-};
+};
\ No newline at end of file
diff --git a/src/data/comments.js b/src/data/comments.js
index f8ca604..91b6e13 100644
--- a/src/data/comments.js
+++ b/src/data/comments.js
@@ -1,12 +1,24 @@
-[
+const comments=[
{
- "id": 1,
+ "id": 100,
"content": "comment1",
"created_at": "2023-04-18T15:09:43Z",
- "post": 1,
+ "post": 2,
+ "author": {
+ "id": 2,
+ "username": "user2"
+ }
+ },
+ {
+ "id": 101,
+ "content": "comment2",
+ "created_at": "2023-04-18T15:09:43Z",
+ "post": 2,
"author": {
"id": 2,
"username": "user2"
}
}
-]
\ No newline at end of file
+]
+
+export default comments
\ No newline at end of file
diff --git a/src/routes/HomePage.jsx b/src/routes/HomePage.jsx
index 621a5e8..dbdb978 100644
--- a/src/routes/HomePage.jsx
+++ b/src/routes/HomePage.jsx
@@ -1,25 +1,33 @@
import { useEffect, useState } from "react";
import { SmallPost } from "../components/Posts";
-import posts from "../data/posts";
-import { Dotting } from "dotting";
+import { getPosts, getTags } from "../apis/api";
+import { Link } from "react-router-dom";
+import { getCookie } from "../utils/cookie";
const HomePage = () => {
+ //get post API call
+
const [tags, setTags] = useState([]);
const [searchTags, setSearchTags] = useState([]);
const [searchValue, setSearchValue] = useState("");
- const [postList, setPostList] = useState(posts);
+ const [postList, setPostList] = useState([]);
useEffect(() => {
- const tagList = posts.reduce((acc, post) => {
- for (let tag of post.tags) {
- acc.add(tag.content);
- }
- return acc;
- }, new Set());
- setTags([...tagList]);
- setSearchTags([...tagList]);
+ const getPostAPI=async ()=>{
+ const posts=await getPosts();
+ setPostList(posts);
+ };
+ getPostAPI();
+ const getTagsAPI=async ()=>{
+ const tags = await getTags();
+ const tagContents=tags.map((tag)=>tag.content);
+ setTags(tagContents);
+ setSearchTags(tagContents);
+ };
+ getTagsAPI();
}, []);
+
const handleChange = (e) => {
const { value } = e.target;
const newTags = tags.filter((tag) => tag.includes(value));
@@ -28,29 +36,14 @@ const HomePage = () => {
const handleTagFilter = (e) => {
const { innerText } = e.target;
- let tag=innerText.slice(1);
- if(tag == searchValue) {
+ if (searchValue === innerText.substring(1)) {
setSearchValue("");
- // setSearchTags(tags);
- setPostList(posts);
+ } else {
+ const activeTag = innerText.substring(1);
+ setSearchValue(activeTag);
}
- else {
- setSearchValue(tag);
- const newTags = tags.filter((postTag) => postTag.includes(tag));
- // setSearchTags(newTags);
-
- let filteredPosts = posts.filter((post) => {
- for (let postTag of post.tags) {
- if (postTag.content === tag) {
- return true;
- }
- }
- return false;
-
- });
- setPostList(filteredPosts);
- }
};
+
return (
{/* */}
@@ -77,13 +70,26 @@ const HomePage = () => {
);
})}
+
+ {postList
+ .filter((post) =>
+ searchValue
+ ? post.tags.find((tag) => tag.content === searchValue)
+ : post
+ )
+ .map((post) => (
+
+ ))}
-
-
- {postList.map((post) => (
-
- ))}
+ {getCookie("access_token") ? (
+
+
+ Post
+
+
+ ) : null}
+
);
};
diff --git a/src/routes/MyInfoPage.jsx b/src/routes/MyInfoPage.jsx
new file mode 100644
index 0000000..c1e4a96
--- /dev/null
+++ b/src/routes/MyInfoPage.jsx
@@ -0,0 +1,56 @@
+import { useEffect, useState } from "react";
+import { EditableInputField } from "../components/ItemEdit";
+import { getUserProfile, updateUserProfile } from "../apis/api";
+
+const MyInfoPage = () => {
+ const [userProfile, setUserProfile] = useState();
+ const validUser = userProfile?.user;
+ useEffect(() => {
+ const getUserFromAPI = async () => {
+ const gotUserProfile = await getUserProfile();
+ setUserProfile(gotUserProfile);
+ // console.log(gotUserProfile);
+ }
+ getUserFromAPI();
+ },[]);
+
+ const handleEdited = (e) => {
+ e.preventDefault();
+ console.log(userProfile);
+ updateUserProfile(userProfile);
+ };
+
+
+ return (
+ validUser &&
+
+
My Info
+ {setUserProfile({...userProfile, user:{...userProfile.user, email:c}})}}
+ handleEdited={handleEdited}
+ />
+ {setUserProfile({...userProfile, user:{...userProfile.user, username:c}})}}
+ handleEdited={handleEdited}
+ />
+ {setUserProfile({...userProfile, college: c})}}
+ handleEdited={handleEdited}
+ />
+ {setUserProfile({...userProfile, major: c})}}
+ handleEdited={handleEdited}
+ />
+
+ );
+};
+
+export default MyInfoPage;
\ No newline at end of file
diff --git a/src/routes/PostCreatePage.jsx b/src/routes/PostCreatePage.jsx
index 85c97bb..30894b6 100644
--- a/src/routes/PostCreatePage.jsx
+++ b/src/routes/PostCreatePage.jsx
@@ -2,72 +2,50 @@ import { useEffect, useState } from "react";
import { BigPost } from "../components/Posts";
import posts from "../data/posts";
import { PostForm } from "../components/Form";
+import { getTags, createPost } from "../apis/api";
+import { useNavigate } from "react-router-dom";
const PostCreatePage = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
// 화면그리기
const [formData, setFormData] = useState({
- id: posts.length,
title: "",
content: "",
- author: { id: posts.length, username: "베이비" },
tags: [],
});
-
- const onSubmit = (e) => {
- //TODO : api connect
- e.preventDefault();
- {/* 추가 👇🏻 */}
- const createdPost = {
- ...formData,
- like_users: [],
- tags: formData.tags.map((tag, idx) => {
- return { id: idx + 1, content: tag };
- }),
+ const navigate = useNavigate();
+
+ const onSubmit = (e) => {
+ e.preventDefault();
+ createPost(formData, navigate);
};
- setFormData(createdPost);
- setIsSubmitted(true);
- {/* 추가 👆🏻 */}
- };
// 기존 태그 불러오기
// TODO : api call(get all tags)
const [tags, setTags] = useState([]);
useEffect(() => {
- const duplicatedTagList = posts.reduce((acc, post) => {
- for (let tag of post.tags) {
- acc.add(tag.content);
- }
-
- return acc;
- }, new Set());
-
- const tagList = [...duplicatedTagList];
-
- setTags([...tagList]);
+ const getTagsAPI = async () => {
+ const tags = await getTags();
+ const tagContents = tags.map((tag) => {
+ return tag.content;
+ });
+ setTags(tagContents);
+ };
+ getTagsAPI();
}, []);
-
return (
- <>
- {isSubmitted ? (
-
-
-
- ) : (
-
- )}
- >
+
);
};
+
export default PostCreatePage;
diff --git a/src/routes/PostDetailPage.jsx b/src/routes/PostDetailPage.jsx
index c59f0ba..846a95b 100644
--- a/src/routes/PostDetailPage.jsx
+++ b/src/routes/PostDetailPage.jsx
@@ -2,23 +2,44 @@ import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { BigPost } from "../components/Posts";
import { Link } from "react-router-dom";
-import posts from "../data/posts";
+import { Comment } from "../components/Comment";
+import { deletePost, getPost, getUser } from "../apis/api";
+import { getCookie } from "../utils/cookie";
const PostDetailPage = () => {
// parameter로 받은 id에 해당하는 post를 찾아서 넣자
// TODO : api call(get post by id)
const { postId } = useParams();
const [post, setPost] = useState();
+
+ const [user, setUser] = useState();
+
useEffect(() => {
- const post = posts.find((post) => post.id === parseInt(postId));
- setPost(post);
+ // const post = posts.find((post) => post.id === parseInt(postId));
+ // setPost(post);
+ const getPostAPI=async ()=>{
+ setPost(await getPost(postId));
+ };
+ getPostAPI();
}, [postId]);
+ useEffect(() => {
+ // access_token이 있으면 유저 정보 가져옴
+ if (getCookie("access_token")) {
+ const getUserAPI = async () => {
+ const currentUser = await getUser();
+ setUser(currentUser);
+ console.log(currentUser);
+ };
+ getUserAPI();
+ }
+ }, []);
const onClickDelete = () => {
- console.log("delete");
// add api call for deleting post here
// add redirect to home page
+ if(window.confirm("정말 삭제?"));
+ deletePost(postId);
};
@@ -27,14 +48,23 @@ const onClickDelete = () => {
{/* post detail component */}
-
-
-
-
-
-
+
+
+ {user?.id === post?.author?.id ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+ {/* user와 post.author가 동일하면 버튼을 리턴, 아니면 null */}
-
)
);
diff --git a/src/routes/PostEditPage.jsx b/src/routes/PostEditPage.jsx
index 9aadef0..a568bf6 100644
--- a/src/routes/PostEditPage.jsx
+++ b/src/routes/PostEditPage.jsx
@@ -4,75 +4,70 @@ import { PostForm } from "../components/Form";
import posts from "../data/posts";
import { Link } from "react-router-dom";
import { BigPost } from "../components/Posts";
+import { getPost, getTags, updatePost } from "../apis/api";
+import { useNavigate } from "react-router-dom";
const PostEditPage = () => {
- const [isSubmitted, setIsSubmitted] = useState(false);
const { postId } = useParams();
- const [formData, setFormData] = useState({});
+ const [formData, setFormData] = useState({
+ title: "",
+ content: "",
+ tags: [],
+ });
- const [tags, setTags] = useState([]);
useEffect(() => {
- const duplicatedTagList = posts.reduce((acc, post) => {
- for (let tag of post.tags) {
- acc.add(tag.content);
- }
-
- return acc;
- }, new Set());
-
- const tagList = [...duplicatedTagList];
+ const getPostAPI = async () => {
+ const post = await getPost(postId);
+ const postFormData = {
+ ...post,
+ tags: post.tags.map((tag) => tag.content),
+ };
+ setFormData(postFormData);
+ };
+ getPostAPI();
+ }, [postId]);
- setTags([...tagList]);
+ const [tags, setTags] = useState([]);
+ useEffect(() => {
+ const getTagsAPI = async () => {
+ const tags = await getTags();
+ const tagContents = tags.map((tag) => {
+ return tag.content;
+ });
+ setTags(tagContents);
+ };
+ getTagsAPI();
}, []);
- const onSubmit = (e) => {
- //TODO : api connect
- e.preventDefault();
+ const navigate = useNavigate();
- {/* 추가 👇🏻 */}
- const createdPost = {
- ...formData,
- like_users: [],
- tags: formData.tags.map((tag, idx) => {
- return { id: idx + 1, content: tag };
- }),
+ const onSubmit = (e) => {
+ e.preventDefault();
+ updatePost(postId, formData, navigate);
};
- setFormData(createdPost);
- setIsSubmitted(true);
- {/* 추가 👆🏻 */}
- };
// 기존 post 불러오기
useEffect(() => {
const post = posts.find((post) => post.id === parseInt(postId));
- const postFormData = { ...post, tags: post.tags.map((tag) => tag.content) };
+ const postFormData = { ...post, tags: post?.tags.map((tag) => tag.content) };
setFormData(postFormData);
}, [postId]);
-
return (
- <>
- {isSubmitted ? (
-
-
-
- ) : (
-
-
-
-
-
-
Edit Post
-
-
-
- )}
- >
+
+
+
+
+
+
Edit Post
+
+
+
);
};
diff --git a/src/routes/SignInPage.jsx b/src/routes/SignInPage.jsx
index 95e282c..c3da044 100644
--- a/src/routes/SignInPage.jsx
+++ b/src/routes/SignInPage.jsx
@@ -1,15 +1,18 @@
import { useState } from "react";
import { SignInForm } from "../components/Form";
+import { signIn } from "../apis/api";
const SignInPage = () => {
const [formData, setFormData] = useState({
username: "",
password: "",
});
- const handleSignInSubmit = () => {
- console.log(formData );
- alert("로그인 완 료!");
+ const handleSignInSubmit = (e) => {
// add api call for sign in here
+ e.preventDefault();
+ signIn(formData);
+ // console.log(formData );
+ // alert("로그인 완 료!");
};
return (
diff --git a/src/routes/SignUpPage.jsx b/src/routes/SignUpPage.jsx
index f5adcd3..16b1002 100644
--- a/src/routes/SignUpPage.jsx
+++ b/src/routes/SignUpPage.jsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { SignUpForm } from "../components/Form";
+import { signUp } from "../apis/api";
const SignUpPage = () => {
const [formData, setFormData] = useState({
@@ -10,10 +11,11 @@ const SignUpPage = () => {
college: "",
major: "",
});
- const [email, setEmail] = useState("");
- const handleSignUpSubmit = () => {
- alert(`${email}로 회원가입 해 줘`);
+ const handleSignUpSubmit = (e) => {
+ e.preventDefault();
// add api call for sign up here
+ signUp(formData);
+ // alert(`${email}로 회원가입 해 줘`);
};
diff --git a/src/utils/cookie.js b/src/utils/cookie.js
new file mode 100644
index 0000000..bd44684
--- /dev/null
+++ b/src/utils/cookie.js
@@ -0,0 +1,21 @@
+// src/utils/cookie.js
+
+import { Cookies } from 'react-cookie';
+
+const cookies = new Cookies()
+
+// 쿠키 설정하는 함수
+// 궁금하실까봐 만들긴 했는데, 우리는 안 쓸거에요!! (쿠키에 토큰 넣어주는 건 서버에서 해주니까요)
+export const setCookie = ( name, value, option) => {
+ return cookies.set( name, value, {...option})
+}
+
+// 쿠키 정보 가져오는 함수
+export const getCookie = ( name ) => {
+ return cookies.get(name)
+}
+
+// 쿠키 정보 삭제하는 함수
+export const removeCookie = ( name ) => {
+ cookies.remove(name)
+}
\ No newline at end of file