From 931fda51e126ee2614a521be70e90b4b0052b24f Mon Sep 17 00:00:00 2001 From: YoonKeumJae Date: Mon, 2 Jun 2025 16:36:19 +0900 Subject: [PATCH 1/8] complete/javascript application add --- complete/javascript/Dockerfile | 22 ++ complete/javascript/nginx.conf | 19 + complete/javascript/package-lock.json | 159 +++++++- complete/javascript/package.json | 4 +- complete/javascript/src/App.jsx | 219 ++--------- complete/javascript/src/api/apiClient.js | 33 ++ complete/javascript/src/api/apiService.js | 81 ++++ complete/javascript/src/components/Button.jsx | 41 -- .../src/components/CommentSection.jsx | 111 ------ .../javascript/src/components/NewPostForm.jsx | 112 ------ .../javascript/src/components/PostCard.jsx | 100 ----- .../javascript/src/components/PostDetail.jsx | 138 ------- .../src/components/comment/CommentInput.jsx | 60 +++ .../src/components/comment/CommentItem.jsx | 102 +++++ .../common/FloatingActionButton.jsx | 26 ++ .../src/components/common/Layout.jsx | 14 + .../src/components/common/NavBar.jsx | 77 ++++ .../javascript/src/components/modal/Modal.jsx | 45 +++ .../src/components/modal/NameInputModal.jsx | 78 ++++ .../src/components/modal/PostingModal.jsx | 86 +++++ .../src/components/post/PostCard.jsx | 115 ++++++ .../javascript/src/context/AuthContext.jsx | 61 +++ complete/javascript/src/pages/HomePage.jsx | 98 +++++ .../javascript/src/pages/PostDetailPage.jsx | 359 ++++++++++++++++++ complete/javascript/src/pages/ProfilePage.jsx | 209 ++++++++++ complete/javascript/src/pages/SearchPage.jsx | 202 ++++++++++ complete/javascript/src/services/api.js | 160 -------- complete/javascript/src/services/mockData.js | 269 ------------- complete/javascript/src/styles/GlobalStyle.js | 1 + complete/javascript/src/styles/theme.js | 33 ++ complete/javascript/vite.config.js | 9 +- 31 files changed, 1902 insertions(+), 1141 deletions(-) create mode 100644 complete/javascript/Dockerfile create mode 100644 complete/javascript/nginx.conf create mode 100644 complete/javascript/src/api/apiClient.js create mode 100644 complete/javascript/src/api/apiService.js delete mode 100644 complete/javascript/src/components/Button.jsx delete mode 100644 complete/javascript/src/components/CommentSection.jsx delete mode 100644 complete/javascript/src/components/NewPostForm.jsx delete mode 100644 complete/javascript/src/components/PostCard.jsx delete mode 100644 complete/javascript/src/components/PostDetail.jsx create mode 100644 complete/javascript/src/components/comment/CommentInput.jsx create mode 100644 complete/javascript/src/components/comment/CommentItem.jsx create mode 100644 complete/javascript/src/components/common/FloatingActionButton.jsx create mode 100644 complete/javascript/src/components/common/Layout.jsx create mode 100644 complete/javascript/src/components/common/NavBar.jsx create mode 100644 complete/javascript/src/components/modal/Modal.jsx create mode 100644 complete/javascript/src/components/modal/NameInputModal.jsx create mode 100644 complete/javascript/src/components/modal/PostingModal.jsx create mode 100644 complete/javascript/src/components/post/PostCard.jsx create mode 100644 complete/javascript/src/context/AuthContext.jsx create mode 100644 complete/javascript/src/pages/HomePage.jsx create mode 100644 complete/javascript/src/pages/PostDetailPage.jsx create mode 100644 complete/javascript/src/pages/ProfilePage.jsx create mode 100644 complete/javascript/src/pages/SearchPage.jsx delete mode 100644 complete/javascript/src/services/api.js delete mode 100644 complete/javascript/src/services/mockData.js create mode 100644 complete/javascript/src/styles/GlobalStyle.js create mode 100644 complete/javascript/src/styles/theme.js diff --git a/complete/javascript/Dockerfile b/complete/javascript/Dockerfile new file mode 100644 index 0000000..a8ddc10 --- /dev/null +++ b/complete/javascript/Dockerfile @@ -0,0 +1,22 @@ +FROM node:16-alpine as build + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --no-cache + +COPY public/ public/ +COPY src/ src/ + +RUN npm run build + +FROM nginx:stable-alpine + +COPY --from=build /app/build /usr/share/nginx/html + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d/ + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/complete/javascript/nginx.conf b/complete/javascript/nginx.conf new file mode 100644 index 0000000..62c30dc --- /dev/null +++ b/complete/javascript/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /api/ { + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://backend:5050; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/complete/javascript/package-lock.json b/complete/javascript/package-lock.json index 2a9e490..7504da7 100644 --- a/complete/javascript/package-lock.json +++ b/complete/javascript/package-lock.json @@ -8,8 +8,10 @@ "name": "github-copilot-bootcamp-react", "version": "0.1.0", "dependencies": { + "axios": "^1.9.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.30.1" }, "devDependencies": { "@playwright-testing-library/test": "^4.5.0", @@ -1097,6 +1099,15 @@ "node": ">=18" } }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", @@ -1806,6 +1817,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1860,6 +1877,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1960,7 +1988,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2103,6 +2130,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2282,6 +2321,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2320,7 +2368,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2425,7 +2472,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2435,7 +2481,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2473,7 +2518,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2486,7 +2530,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2955,6 +2998,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -2988,6 +3051,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3028,7 +3106,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3079,7 +3156,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3104,7 +3180,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3198,7 +3273,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3270,7 +3344,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3283,7 +3356,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3299,7 +3371,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4046,7 +4117,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4076,6 +4146,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4746,6 +4837,12 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4819,6 +4916,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/complete/javascript/package.json b/complete/javascript/package.json index 49af088..aae0aa0 100644 --- a/complete/javascript/package.json +++ b/complete/javascript/package.json @@ -10,8 +10,10 @@ "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { + "axios": "^1.9.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.30.1" }, "devDependencies": { "@playwright-testing-library/test": "^4.5.0", diff --git a/complete/javascript/src/App.jsx b/complete/javascript/src/App.jsx index ab8f033..577b5fe 100644 --- a/complete/javascript/src/App.jsx +++ b/complete/javascript/src/App.jsx @@ -1,199 +1,32 @@ -import React, { useState, useEffect } from 'react'; -import PostCard from './components/PostCard'; -import NewPostForm from './components/NewPostForm'; -import PostDetail from './components/PostDetail'; -import { getPosts, deletePost } from './services/api'; +import { + BrowserRouter as Router, + Routes, + Route, + Navigate, +} from "react-router-dom"; +import { AuthProvider } from "./context/AuthContext"; -function App() { - const [posts, setPosts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [userName, setUserName] = useState(() => { - return localStorage.getItem('userName') || ''; - }); - const [selectedPostId, setSelectedPostId] = useState(null); - - // 로컬 스토리지에 사용자 이름 저장 - useEffect(() => { - if (userName) { - localStorage.setItem('userName', userName); - } - }, [userName]); - - // 포스트 목록 불러오기 - const loadPosts = async () => { - setIsLoading(true); - setError(null); - - try { - const fetchedPosts = await getPosts(); - setPosts(fetchedPosts); - } catch (err) { - console.error('포스트 불러오기 오류:', err); - setError('포스트를 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - } - }; - - // 컴포넌트 마운트 시 포스트 불러오기 - useEffect(() => { - loadPosts(); - }, []); - - // 포스트 클릭 핸들러 - const handlePostClick = (postId) => { - setSelectedPostId(postId); - }; - - // 포스트 삭제 핸들러 - const handleDeletePost = async (postId) => { - if (!window.confirm('정말로 이 포스트를 삭제하시겠습니까?')) { - return; - } - - try { - await deletePost(postId); - setPosts(posts.filter(post => post.id !== postId)); - } catch (err) { - console.error('포스트 삭제 오류:', err); - alert('포스트 삭제에 실패했습니다.'); - } - }; +import HomePage from "./pages/HomePage"; +import PostDetailPage from "./pages/PostDetailPage"; +import SearchPage from "./pages/SearchPage"; +import ProfilePage from "./pages/ProfilePage"; +function App() { return ( -
-
-
-

Contoso 아웃도어 소셜

-

아웃도어 컴퍼니를 위한 소셜 미디어 플랫폼

-
-
- -
-
-
- {/* 왼쪽 사이드바 - 데스크톱에서만 표시 */} -
-
-

Contoso 아웃도어

-

- 최고 품질의 아웃도어 장비와 액세서리를 제공합니다. - 자연을 탐험하고 모험을 즐기세요! -

-
-
-

인기 태그

-
- #등산 - #캠핑 - #트레킹 - #아웃도어 - #백패킹 -
-
-
-
- - {/* 메인 콘텐츠 영역 */} -
- {/* 새 포스트 폼 */} - - - {/* 포스트 목록 */} -
-

최근 포스트

- - {error && ( -
- {error} -
- )} - - {isLoading ? ( -
-

포스트를 불러오는 중...

-
- ) : posts.length === 0 ? ( -
-

아직 포스트가 없습니다.

-

첫 번째 포스트를 작성해 보세요!

-
- ) : ( -
- {posts.map((post) => ( -
handlePostClick(post.id)} className="cursor-pointer"> - { - e.stopPropagation(); - handleDeletePost(post.id); - }} - refreshPosts={loadPosts} - userName={userName} - /> -
- ))} -
- )} -
-
- - {/* 오른쪽 사이드바 - 데스크톱에서만 표시 */} -
-
-

신제품 소식

-
    -
  • -

    새로운 경량 텐트 출시

    -

    초경량 2인용 텐트를 지금 만나보세요!

    -
  • -
  • -

    여름 시즌 하이킹 부츠

    -

    더운 여름을 위한 특별 설계 부츠

    -
  • -
  • -

    방수 재킷 할인 이벤트

    -

    이번 주까지 전 제품 20% 할인

    -
  • -
-
- -
-

사용 안내

-

- Contoso 아웃도어 제품과 관련된 경험을 공유하고 다른 사용자들과 소통하세요. -

-
    -
  • 제품 사진과 함께 리뷰를 남겨주세요
  • -
  • 다른 사용자들의 포스트에 댓글을 달아보세요
  • -
  • 유용한 팁과 노하우를 공유해 보세요
  • -
-
-
-
-
-
- - {/* 푸터 */} -
-
-

- © 2025 Contoso 아웃도어 컴패니. All rights reserved. -

-
-
- - {/* 포스트 상세 모달 */} - {selectedPostId && ( - setSelectedPostId(null)} - onDeleteSuccess={loadPosts} - userName={userName} - /> - )} -
+ + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/complete/javascript/src/api/apiClient.js b/complete/javascript/src/api/apiClient.js new file mode 100644 index 0000000..33f0bca --- /dev/null +++ b/complete/javascript/src/api/apiClient.js @@ -0,0 +1,33 @@ +import axios from "axios"; + +const apiClient = axios.create({ + baseURL: "/api", + headers: { + "Content-Type": "application/json", + }, +}); + +apiClient.interceptors.request.use( + (config) => { + const userStr = localStorage.getItem("user"); + if (userStr) { + try { + const user = JSON.parse(userStr); + if (user.userId) { + config.headers["X-User-ID"] = user.userId; + } + if (user.username) { + config.headers["x-username"] = encodeURIComponent(user.username); + } + } catch (error) { + console.error("Error parsing user information:", error); + } + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/complete/javascript/src/api/apiService.js b/complete/javascript/src/api/apiService.js new file mode 100644 index 0000000..6d036f3 --- /dev/null +++ b/complete/javascript/src/api/apiService.js @@ -0,0 +1,81 @@ +import apiClient from "./apiClient"; + +export const authApi = { + login: (username) => apiClient.post("/login", { username }), +}; + +export const postApi = { + getPosts: (page = 1, limit = 10) => + apiClient.get(`/posts?page=${page}&limit=${limit}`), + + getPost: (postId) => apiClient.get(`/posts/${postId}`), + + createPost: (content, username) => + apiClient.post("/posts", { content, username }), + + updatePost: (postId, content, username) => { + return apiClient.put( + `/posts/${postId}`, + { content }, + { params: { username } } + ); + }, + + deletePost: (postId, username) => { + return apiClient.delete(`/posts/${postId}`, { + params: { username }, + }); + }, + + likePost: (postId, username) => { + return apiClient.post(`/posts/${postId}/like`, null, { + params: { username }, + }); + }, + + unlikePost: (postId, username) => { + return apiClient.delete(`/posts/${postId}/like`, { + params: { username }, + }); + }, +}; + +export const commentApi = { + getComments: (postId, page = 1, limit = 10) => + apiClient.get(`/posts/${postId}/comments?page=${page}&limit=${limit}`), + + createComment: (postId, content, username) => + apiClient.post(`/posts/${postId}/comments`, { content, username }), + + updateComment: (commentId, content, username) => { + return apiClient.put( + `/comments/${commentId}`, + { content }, + { + params: { username }, + } + ); + }, + + deleteComment: (commentId, username) => { + return apiClient.delete(`/comments/${commentId}`, { + params: { username }, + }); + }, +}; + +export const searchApi = { + searchUsers: (username, page = 1, limit = 10) => { + return apiClient.get(`/search`, { + params: { + username, + page, + limit, + }, + }); + }, +}; + +export const userApi = { + getUserProfile: (userId) => apiClient.get(`/users/${userId}`), +}; diff --git a/complete/javascript/src/components/Button.jsx b/complete/javascript/src/components/Button.jsx deleted file mode 100644 index f4b4837..0000000 --- a/complete/javascript/src/components/Button.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -const Button = ({ - children, - type = "button", - variant = "primary", - onClick, - className = "", - disabled = false, - small = false -}) => { - - const baseStyles = "inline-flex items-center justify-center rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors"; - - const sizeStyles = small - ? "px-3 py-1.5 text-sm" - : "px-4 py-2 text-base"; - - const variantStyles = { - primary: "bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 text-white", - secondary: "bg-gray-100 hover:bg-gray-200 focus:ring-gray-500 text-gray-800", - danger: "bg-red-600 hover:bg-red-700 focus:ring-red-500 text-white", - success: "bg-green-600 hover:bg-green-700 focus:ring-green-500 text-white", - outline: "bg-white border border-gray-300 hover:bg-gray-50 focus:ring-blue-500 text-gray-700" - }; - - const disabledStyles = disabled ? "opacity-50 cursor-not-allowed" : ""; - - return ( - - ); -}; - -export default Button; \ No newline at end of file diff --git a/complete/javascript/src/components/CommentSection.jsx b/complete/javascript/src/components/CommentSection.jsx deleted file mode 100644 index 82a3af8..0000000 --- a/complete/javascript/src/components/CommentSection.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Button from './Button'; -import { getComments, createComment } from '../services/api'; - -const CommentSection = ({ postId, userName }) => { - const [comments, setComments] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [newComment, setNewComment] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - - // 댓글 목록 불러오기 - const fetchComments = async () => { - setIsLoading(true); - setError(''); - - try { - const fetchedComments = await getComments(postId); - setComments(fetchedComments); - } catch (err) { - console.error('댓글 불러오기 오류:', err); - setError('댓글을 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - } - }; - - // 컴포넌트 마운트 시 댓글 불러오기 - useEffect(() => { - fetchComments(); - }, [postId]); - - // 새 댓글 작성 처리 - const handleSubmitComment = async (e) => { - e.preventDefault(); - - if (!newComment.trim()) return; - - setIsSubmitting(true); - - try { - const commentData = { - userName, - content: newComment - }; - - const createdComment = await createComment(postId, commentData); - setComments([...comments, createdComment]); - setNewComment(''); - } catch (err) { - console.error('댓글 작성 오류:', err); - setError('댓글 작성에 실패했습니다.'); - } finally { - setIsSubmitting(false); - } - }; - - // 날짜 포맷팅 - const formatDate = (dateString) => { - const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; - return new Date(dateString).toLocaleDateString('ko-KR', options); - }; - - return ( -
-

댓글

- - {error &&

{error}

} - - {isLoading ? ( -

댓글 불러오는 중...

- ) : ( -
- {comments.length === 0 ? ( -

아직 댓글이 없습니다.

- ) : ( - comments.map(comment => ( -
-
- {comment.userName} - {formatDate(comment.createdAt)} -
-

{comment.content}

-
- )) - )} -
- )} - -
-
-