diff --git a/.gitignore b/.gitignore
index a547bf3..3b0b403 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+.env
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..62c3bc2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,41 @@
+# Base image for building
+FROM node:18-alpine AS base
+
+# Define arguments for environment variables
+ARG VITE_KAKAO_MAP_KEY
+ARG VITE_KAKAO_REST_API_KEY
+
+# Install dependencies
+WORKDIR /app
+COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
+RUN \
+ if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
+ elif [ -f package-lock.json ]; then npm ci; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+# Copy source code and set environment variables
+COPY . .
+ENV VITE_KAKAO_MAP_KEY=${VITE_KAKAO_MAP_KEY}
+ENV VITE_KAKAO_REST_API_KEY=${VITE_KAKAO_REST_API_KEY}
+
+# Build the Vite application
+RUN \
+ if [ -f yarn.lock ]; then yarn build; \
+ elif [ -f package-lock.json ]; then npm run build; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+# Nginx server to serve static files
+FROM nginx:stable-alpine AS runner
+
+# Copy built files to Nginx for serving
+COPY --from=base /app/dist /usr/share/nginx/html
+
+# Expose port 80 for Nginx
+EXPOSE 80
+
+# Start Nginx
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/eslint.config.js b/eslint.config.js
index 5e99aa9..2209cff 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -24,5 +24,6 @@ export default [
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-explicit-any": "off",
},
+ env: { node: true },
},
];
diff --git a/index.html b/index.html
index 58147d5..388939c 100644
--- a/index.html
+++ b/index.html
@@ -11,6 +11,11 @@
/>
포켓네컷
+
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 0000000..7d9fe86
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,19 @@
+server {
+ listen 80;
+ server_name pocket4cut.link;
+
+ # 정적 파일 서빙
+ location / {
+ try_files $uri /index.html;
+ }
+
+ # API 및 SSR 요청 프록시 - 기본 요청을 애플리케이션 서버로 프록시
+ location / {
+ proxy_pass http://localhost:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7208763..3824fa7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,23 +10,35 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
+ "@tanstack/react-query": "^5.59.0",
+ "@types/react-slick": "^0.23.13",
"@types/styled-components": "^5.1.34",
+ "@yudiel/react-qr-scanner": "^2.0.8",
"autoprefixer": "^10.4.20",
+ "axios": "^1.7.7",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-styled-components": "^2.1.4",
"postcss": "^8.4.45",
"postcss-import": "^16.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.3.0",
+ "react-kakao-maps-sdk": "^1.1.27",
"react-router-dom": "^6.26.2",
+ "react-slick": "^0.30.2",
+ "react-webcam": "^7.2.0",
+ "slick-carousel": "^1.8.1",
"styled-components": "^6.1.13",
"tailwind-styled-components": "^2.2.0",
"tailwindcss": "^3.4.10",
"twin.macro": "^3.4.1",
- "vite-plugin-babel-macros": "^1.0.6"
+ "vite-plugin-babel-macros": "^1.0.6",
+ "zustand": "^5.0.0-rc.2"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
+ "@tanstack/react-query-devtools": "^5.59.15",
+ "@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
@@ -37,6 +49,7 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
+ "kakao.maps.d.ts": "^0.1.40",
"prettier": "^3.3.3",
"typescript": "^5.5.3",
"typescript-eslint": "^8.5.0",
@@ -1228,6 +1241,18 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -1780,6 +1805,61 @@
"@svgr/core": "*"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.59.13",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz",
+ "integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.58.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.58.0.tgz",
+ "integrity": "sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.59.15",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz",
+ "integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.59.13"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.59.15",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.15.tgz",
+ "integrity": "sha512-rX28KTivkA2XEn3Fj9ckDtnTPY8giWYgssySSAperpVol4+th+NCij/MhLylfB+Mfg2JfCxOcwnM/fwzS8iSog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.58.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.59.15",
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1821,6 +1901,16 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/dom-webcodecs": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz",
+ "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="
+ },
+ "node_modules/@types/emscripten": {
+ "version": "1.39.13",
+ "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz",
+ "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw=="
+ },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -1837,6 +1927,16 @@
"hoist-non-react-statics": "^3.3.0"
}
},
+ "node_modules/@types/node": {
+ "version": "22.5.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
+ "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -1869,6 +1969,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-slick": {
+ "version": "0.23.13",
+ "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
+ "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/styled-components": {
"version": "5.1.34",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz",
@@ -2148,11 +2257,24 @@
"vite": "^4.2.0 || ^5.0.0"
}
},
+ "node_modules/@yudiel/react-qr-scanner": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-2.0.8.tgz",
+ "integrity": "sha512-/7WHsdC1a/Z909J2zZxqgpUQ1iI554fZxIagucH/tFu1MhZkNIeykYI1OdZgDEwV4CzuSi+h90wwNrhmETcmRw==",
+ "dependencies": {
+ "barcode-detector": "^2.2.7",
+ "webrtc-adapter": "9.0.1"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -2378,6 +2500,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -2431,6 +2559,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+ "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -2485,6 +2624,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/barcode-detector": {
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.11.tgz",
+ "integrity": "sha512-N50XZ6Rav2sxTgHXOc38/mkpVJMan11GZ8Yqi1pPMZpTJSXuZ/FpIee6OtLehZX/Vs4ZOzGbp1DgXzFCfKggWA==",
+ "dependencies": {
+ "@types/dom-webcodecs": "^0.1.13",
+ "zxing-wasm": "1.2.14"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -2552,6 +2700,14 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -2681,6 +2837,12 @@
"node": ">= 6"
}
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2696,6 +2858,18 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"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",
@@ -2900,6 +3074,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",
@@ -2953,6 +3136,12 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
+ "node_modules/enquire.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
+ "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==",
+ "license": "MIT"
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -3678,6 +3867,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.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -3704,6 +3913,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "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",
@@ -4517,6 +4740,13 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jquery": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4575,6 +4805,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-convert": "^0.2.0"
+ }
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -4603,6 +4842,12 @@
"node": ">=4.0"
}
},
+ "node_modules/kakao.maps.d.ts": {
+ "version": "0.1.40",
+ "resolved": "https://registry.npmjs.org/kakao.maps.d.ts/-/kakao.maps.d.ts-0.1.40.tgz",
+ "integrity": "sha512-nX69MB1ok04epe3OqS+/tEeWBbU31GSQbvDPJmQRRltzzqn6t4jBsO5v1nzalUjCKzwcH2CptOc767NZ7Hbu3g==",
+ "license": "MIT"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4664,6 +4909,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -4728,6 +4979,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",
@@ -5361,6 +5633,12 @@
"react-is": "^16.13.1"
}
},
+ "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",
@@ -5416,12 +5694,35 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-icons": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
+ "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"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==",
"license": "MIT"
},
+ "node_modules/react-kakao-maps-sdk": {
+ "version": "1.1.27",
+ "resolved": "https://registry.npmjs.org/react-kakao-maps-sdk/-/react-kakao-maps-sdk-1.1.27.tgz",
+ "integrity": "sha512-1EwYkYsjTDRFqysKStDasFMrFTXcLx2AyRlqMoWD7ONWhRqpjx9M874hkhEEHrnypP2eSIhhDLe0EiSKp3bd2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.15",
+ "kakao.maps.d.ts": "^0.1.39"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -5464,6 +5765,32 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-slick": {
+ "version": "0.30.2",
+ "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz",
+ "integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==",
+ "license": "MIT",
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "enquire.js": "^2.1.6",
+ "json2mq": "^0.2.0",
+ "lodash.debounce": "^4.0.8",
+ "resize-observer-polyfill": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-webcam": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz",
+ "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==",
+ "peerDependencies": {
+ "react": ">=16.2.0",
+ "react-dom": ">=16.2.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -5532,6 +5859,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -5673,6 +6006,11 @@
"loose-envify": "^1.1.0"
}
},
+ "node_modules/sdp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
+ "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5774,6 +6112,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/slick-carousel": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
+ "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "jquery": ">=1.8.0"
+ }
+ },
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@@ -5802,6 +6149,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+ "license": "MIT"
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -6226,6 +6602,34 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/terser": {
+ "version": "5.32.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.32.0.tgz",
+ "integrity": "sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6535,6 +6939,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -6685,6 +7096,18 @@
"vite": "^2.6.0 || 3 || 4 || 5"
}
},
+ "node_modules/webrtc-adapter": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
+ "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
+ "dependencies": {
+ "sdp": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=6.0.0",
+ "npm": ">=3.10.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6947,6 +7370,43 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0-rc.2.tgz",
+ "integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zxing-wasm": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.2.14.tgz",
+ "integrity": "sha512-UaYfzSmFPIEmYDt/KyPvs/H02t8jO470BBFHUIlvtmloAm8f2zdAmOL93iWYQ5QYfSnTyFPg0yzZwznlBjfg4A==",
+ "dependencies": {
+ "@types/emscripten": "^1.39.13"
+ }
}
}
}
diff --git a/package.json b/package.json
index 8198f18..ace0505 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint './src/**/*.{ts,tsx,js,jsx}'",
"lint:fix": "eslint --fix './src/**/*.{ts,tsx,js,jsx}'",
@@ -14,23 +14,35 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
+ "@tanstack/react-query": "^5.59.0",
+ "@types/react-slick": "^0.23.13",
"@types/styled-components": "^5.1.34",
+ "@yudiel/react-qr-scanner": "^2.0.8",
"autoprefixer": "^10.4.20",
+ "axios": "^1.7.7",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-styled-components": "^2.1.4",
"postcss": "^8.4.45",
"postcss-import": "^16.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.3.0",
+ "react-kakao-maps-sdk": "^1.1.27",
"react-router-dom": "^6.26.2",
+ "react-slick": "^0.30.2",
+ "react-webcam": "^7.2.0",
+ "slick-carousel": "^1.8.1",
"styled-components": "^6.1.13",
"tailwind-styled-components": "^2.2.0",
"tailwindcss": "^3.4.10",
"twin.macro": "^3.4.1",
- "vite-plugin-babel-macros": "^1.0.6"
+ "vite-plugin-babel-macros": "^1.0.6",
+ "zustand": "^5.0.0-rc.2"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
+ "@tanstack/react-query-devtools": "^5.59.15",
+ "@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
@@ -41,6 +53,7 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
+ "kakao.maps.d.ts": "^0.1.40",
"prettier": "^3.3.3",
"typescript": "^5.5.3",
"typescript-eslint": "^8.5.0",
diff --git a/src/@types/api.ts b/src/@types/api.ts
new file mode 100644
index 0000000..be5abc6
--- /dev/null
+++ b/src/@types/api.ts
@@ -0,0 +1,13 @@
+export type CommonResponse = {
+ result: {
+ code: number;
+ message: string;
+ };
+ payload: T;
+};
+
+export type CommonError = {
+ errorCode: string;
+ message: string;
+ success: boolean;
+};
diff --git a/src/@types/booth.ts b/src/@types/booth.ts
new file mode 100644
index 0000000..60b699a
--- /dev/null
+++ b/src/@types/booth.ts
@@ -0,0 +1,15 @@
+export type BoothInfo = {
+ id: number;
+ name: string;
+ brand: string;
+ x: number;
+ y: number;
+};
+
+export type SpecificBoothInfo = {
+ name: string;
+ road: string;
+ photoBoothBrand: string;
+ x: number;
+ y: number;
+};
diff --git a/src/@types/review.ts b/src/@types/review.ts
new file mode 100644
index 0000000..c9281c5
--- /dev/null
+++ b/src/@types/review.ts
@@ -0,0 +1,21 @@
+export type Review = {
+ photoboothId?: number;
+ name: string;
+ year: number;
+ month: string;
+ date: string;
+ contents: string;
+ features: string[];
+ imageUrl: string;
+ imagesCount: number;
+};
+
+export type Feature = {
+ id: number;
+ featureName: string;
+};
+
+export type TagCnt = {
+ featureName: string;
+ count: number;
+};
diff --git a/src/@types/user.ts b/src/@types/user.ts
new file mode 100644
index 0000000..d30db9e
--- /dev/null
+++ b/src/@types/user.ts
@@ -0,0 +1,5 @@
+export type UserData = {
+ name: string;
+ email: string;
+ image: string;
+};
diff --git a/src/App.tsx b/src/App.tsx
index ab9c348..67442a6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,6 +3,27 @@ import Home from "./pages/Home";
import SplashScreen from "./pages/SplashScreen";
import Album from "./pages/Album";
import My from "./pages/My";
+import PhotoUpload from "./pages/PhotoUpload";
+
+import QRScan from "./pages/QRScan";
+import PhotoReview from "./pages/PhotoReview";
+import PhotoCheck from "./pages/PhotoCheck";
+
+import LoginPage from "./pages/LoginPage";
+import Token from "./pages/Token";
+import FeedPage from "./pages/Booth/FeedPage";
+import ReviewPage from "./pages/Booth/ReviewPage";
+import ImagePage from "./pages/Booth/ImagePage";
+import BoothDetail from "./pages/Booth";
+import WriteReview from "./pages/WriteReview";
+import Step1 from "./pages/WriteReview/step1";
+import Step2 from "./pages/WriteReview/step2";
+import CompleteScreen from "./pages/CompleteScreen";
+import RecordPage from "./pages/My/RecordPage";
+import FavoritesPage from "./pages/My/FavoritesPage";
+import MyReviewPage from "./pages/My/MyReviewPage";
+import LikeBoothsPage from "./pages/My/LikeBoothsPage";
+import VisitedBoothsPage from "./pages/My/VisitedBoothsPage";
function App() {
return (
@@ -11,7 +32,33 @@ function App() {
} />
} />
} />
- } />
+
+ } />
+
+ } />
+ } />
+ } />
+
+ } />
+ } />
+
+ }>
+ } />
+ } />
+ } />
+
+ }>
+ } />
+ } />
+
+ } />
+ } />
+ } />
+ }>
+ } />
+ } />
+
+ } />
);
diff --git a/src/api/booth.ts b/src/api/booth.ts
new file mode 100644
index 0000000..03ddebf
--- /dev/null
+++ b/src/api/booth.ts
@@ -0,0 +1,48 @@
+import { Get } from ".";
+import { BoothInfo, SpecificBoothInfo } from "../@types/booth";
+
+export const getBoothLatLng = async (lat: number, lng: number, brands: string[], token: string) => {
+ let queryParams;
+ if (brands?.length > 0) {
+ queryParams = brands.map((brand) => `lat=${lat}&lon=${lng}&brand=${brand}`).join("&");
+ } else {
+ queryParams = `lat=${lat}&lon=${lng}`;
+ }
+
+ try {
+ const res = await Get(`/api/v1/photobooth?${queryParams}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getBoothInfo = async (id: string, token: string) => {
+ try {
+ const res = await Get(`/api/v1/photobooth/${id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const searchPhotoBoothName = async (id: string, token: string) => {
+ try {
+ const res = await Get(`/api/v1/photobooth/name/${id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
diff --git a/src/api/file.ts b/src/api/file.ts
new file mode 100644
index 0000000..01fa68a
--- /dev/null
+++ b/src/api/file.ts
@@ -0,0 +1,38 @@
+import axios from "axios";
+import { Post } from ".";
+
+export const getPresignedUrl = async (prefix: string, fileName: string, accessToken: string) => {
+ try {
+ const res = await Post<{
+ url: string;
+ filePath: string;
+ }>(
+ "/api/v1/file",
+ {
+ prefix: prefix,
+ fileName: fileName,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ );
+
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const uploadToS3 = async (presignedUrl: string, file: File) => {
+ try {
+ await axios.put(presignedUrl, file, {
+ headers: {
+ "Content-Type": file.type,
+ },
+ });
+ } catch (error) {
+ console.log(error);
+ }
+};
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 0000000..534828e
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1,23 @@
+import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
+import { CommonResponse } from "../@types/api";
+
+export const axiosInstance = axios.create({
+ baseURL: "https://pocket4cut.link",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ withCredentials: true,
+});
+
+export const Get = async (url: string, config?: AxiosRequestConfig): Promise>> => {
+ const response = await axiosInstance.get(url, config);
+ return response;
+};
+export const Post = async (
+ url: string,
+ body: any,
+ config?: AxiosRequestConfig
+): Promise>> => {
+ const response = await axiosInstance.post(url, body, config);
+ return response;
+};
diff --git a/src/api/review.ts b/src/api/review.ts
new file mode 100644
index 0000000..b6f2628
--- /dev/null
+++ b/src/api/review.ts
@@ -0,0 +1,144 @@
+import { Get, Post } from ".";
+import { Feature, Review, TagCnt } from "../@types/review";
+
+export const submitReviewData = async (
+ accessToken: string,
+ photoboothId: string,
+ rating: number,
+ boothFeatures: number[],
+ photoFeatures: number[],
+ filePaths: string[],
+ content: string
+) => {
+ const contentText = content === "" ? null : content;
+
+ try {
+ const res = await Post(
+ "/api/v1/review",
+ {
+ photoboothId: photoboothId,
+ rating: rating,
+ boothFeatures: boothFeatures,
+ photoFeatures: photoFeatures,
+ filePaths: filePaths,
+ content: contentText,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ );
+ return res.data.result;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getRating = async (boothId: string, accessToken: string) => {
+ try {
+ const res = await Get(`/api/v1/photobooth/rating/${boothId}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getRecentImages = async (boothId: string, accessToken: string) => {
+ try {
+ const res = await Get<{
+ filePaths: string[];
+ totalImageCount: number;
+ }>(`/api/v1/review/images/${boothId}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getRecentReviews = async (boothId: string, accessToken: string) => {
+ try {
+ const res = await Get<{
+ reviewCount: number;
+ reviews: Review[];
+ }>(`/api/v1/review/reviews/${boothId}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getBoothTags = async (boothId: string, accessToken: string) => {
+ try {
+ const res = await Get(`/api/v1/review/boothfeatures/${boothId}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const getPhotoTags = async (boothId: string, accessToken: string) => {
+ try {
+ const res = await Get(`/api/v1/review/photofeatures/${boothId}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const searchPhotoFeatures = async (accessToken: string) => {
+ try {
+ const res = await Get(`/api/v1/review/allphotofeature`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const searchBoothFeatures = async (accessToken: string) => {
+ try {
+ const res = await Get(`/api/v1/review/allboothfeature`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
+export const getSixImages = async (boothId: string) => {
+ try {
+ const res = await Get(`/api/v1/review/images/${boothId}`);
+
+ return res.data.payload;
+ } catch (error) {}
+};
diff --git a/src/api/user.ts b/src/api/user.ts
new file mode 100644
index 0000000..0e0636e
--- /dev/null
+++ b/src/api/user.ts
@@ -0,0 +1,15 @@
+import { Get } from ".";
+import { UserData } from "../@types/user";
+
+export const getUserInfo = async (accessToken: string) => {
+ try {
+ const res = await Get("/v1/api/user", {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ return res.data.payload;
+ } catch (error) {
+ console.log(error);
+ }
+};
diff --git a/src/assets/icons/active-booth-marker.tsx b/src/assets/icons/active-booth-marker.tsx
new file mode 100644
index 0000000..0fcf5fc
--- /dev/null
+++ b/src/assets/icons/active-booth-marker.tsx
@@ -0,0 +1,47 @@
+type MarkerProps = {
+ width: number;
+ height: number;
+ imageUrl: string;
+ color: string;
+};
+
+const ActiveCustomMarker = ({ width, height, imageUrl, color }: MarkerProps) => {
+ // 마커 크기의 80%를 계산
+ const imageSize = width * 0.55;
+
+ const patternId = `pattern-${Math.random().toString(36).substr(2, 9)}`;
+ return (
+
+ );
+};
+
+export default ActiveCustomMarker;
+
+;
diff --git a/src/assets/icons/album-delete-icon.tsx b/src/assets/icons/album-delete-icon.tsx
new file mode 100644
index 0000000..9ca6094
--- /dev/null
+++ b/src/assets/icons/album-delete-icon.tsx
@@ -0,0 +1,23 @@
+import { IconProps } from "../../@types/icon";
+
+export default function AlbumDeleteIcon({ width, height, color }: IconProps) {
+ return (
+
+);
+}
diff --git a/src/assets/icons/album-like-icon.tsx b/src/assets/icons/album-like-icon.tsx
new file mode 100644
index 0000000..fb93e66
--- /dev/null
+++ b/src/assets/icons/album-like-icon.tsx
@@ -0,0 +1,12 @@
+import { IconProps } from "../../@types/icon";
+
+export default function AlbumLikeIcon({ width, height, color }: IconProps) {
+ return (
+
+
+);
+}
diff --git a/src/assets/icons/back-icon.tsx b/src/assets/icons/back-icon.tsx
new file mode 100644
index 0000000..d5aec0a
--- /dev/null
+++ b/src/assets/icons/back-icon.tsx
@@ -0,0 +1,17 @@
+import { IconProps } from "../../@types/icon";
+
+function BackIcon({ color }: IconProps) {
+ return (
+
+ );
+}
+
+export default BackIcon;
diff --git a/src/assets/icons/booth-marker.tsx b/src/assets/icons/booth-marker.tsx
new file mode 100644
index 0000000..866737b
--- /dev/null
+++ b/src/assets/icons/booth-marker.tsx
@@ -0,0 +1,40 @@
+type MarkerProps = {
+ width: number;
+ height: number;
+ imageUrl: string;
+ color: string;
+};
+
+const CustomMarker = ({ width, height, imageUrl, color }: MarkerProps) => {
+ // 마커 크기의 80%를 계산
+ const imageSize = width * 0.7;
+ // 고유한 패턴 ID를 useMemo로 한 번만 생성
+ const patternId = `pattern-${Math.random().toString(36).substr(2, 9)}`;
+ return (
+
+ );
+};
+
+export default CustomMarker;
diff --git a/src/assets/icons/close-icon.tsx b/src/assets/icons/close-icon.tsx
new file mode 100644
index 0000000..6323cc4
--- /dev/null
+++ b/src/assets/icons/close-icon.tsx
@@ -0,0 +1,28 @@
+import { IconProps } from "../../@types/icon";
+
+function CloseIcon({ color }: IconProps) {
+ return (
+
+ );
+}
+
+export default CloseIcon;
diff --git a/src/assets/icons/edit-icon.tsx b/src/assets/icons/edit-icon.tsx
new file mode 100644
index 0000000..d655f4e
--- /dev/null
+++ b/src/assets/icons/edit-icon.tsx
@@ -0,0 +1,31 @@
+import { IconProps } from "../../@types/icon";
+
+function EditIcon({ width, height, color }: IconProps) {
+ return (
+
+ );
+}
+
+export default EditIcon;
diff --git a/src/assets/icons/like-filled-icon.tsx b/src/assets/icons/like-filled-icon.tsx
new file mode 100644
index 0000000..128dc67
--- /dev/null
+++ b/src/assets/icons/like-filled-icon.tsx
@@ -0,0 +1,22 @@
+import { IconProps } from "../../@types/icon";
+
+function LikeFilledIcon({ width, height, color }: IconProps) {
+ return (
+
+ );
+}
+
+export default LikeFilledIcon;
diff --git a/src/assets/icons/like-not-filled-icon.tsx b/src/assets/icons/like-not-filled-icon.tsx
new file mode 100644
index 0000000..68dd74b
--- /dev/null
+++ b/src/assets/icons/like-not-filled-icon.tsx
@@ -0,0 +1,20 @@
+import { IconProps } from "../../@types/icon";
+
+function LikeNotFilledIcon({ width, height, color }: IconProps) {
+ return (
+
+ );
+}
+
+export default LikeNotFilledIcon;
diff --git a/src/assets/icons/place-icon.tsx b/src/assets/icons/place-icon.tsx
new file mode 100644
index 0000000..576f80f
--- /dev/null
+++ b/src/assets/icons/place-icon.tsx
@@ -0,0 +1,13 @@
+function PlaceIcon() {
+ return (
+
+ );
+}
+
+export default PlaceIcon;
diff --git a/src/assets/icons/progress-icon.tsx b/src/assets/icons/progress-icon.tsx
new file mode 100644
index 0000000..0a1e74c
--- /dev/null
+++ b/src/assets/icons/progress-icon.tsx
@@ -0,0 +1,142 @@
+type ProgressIconProps = {
+ percentage: number;
+}
+
+const ProgressIcon = ({ percentage } : ProgressIconProps) => {
+ const radius = 96.3043;
+ const circumference = 2 * Math.PI * radius;
+ const progress = (percentage / 100) * circumference;
+
+ return (
+
+
+
+
+ {percentage}%
+
+
+ );
+};
+
+export default ProgressIcon;
diff --git a/src/assets/icons/right-arrow.tsx b/src/assets/icons/right-arrow.tsx
new file mode 100644
index 0000000..0ea1d54
--- /dev/null
+++ b/src/assets/icons/right-arrow.tsx
@@ -0,0 +1,23 @@
+import { IconProps } from "../../@types/icon";
+
+function RightArrowIcon({ width, height, color }: IconProps) {
+ return (
+
+ );
+}
+
+export default RightArrowIcon;
diff --git a/src/assets/icons/search-icon.tsx b/src/assets/icons/search-icon.tsx
new file mode 100644
index 0000000..2a143e9
--- /dev/null
+++ b/src/assets/icons/search-icon.tsx
@@ -0,0 +1,29 @@
+import { IconProps } from "../../@types/icon";
+
+function SearchIcon({ width, height, color }: IconProps) {
+ return (
+
+ );
+}
+
+export default SearchIcon;
diff --git a/src/assets/icons/star-icon.tsx b/src/assets/icons/star-icon.tsx
new file mode 100644
index 0000000..9e81c34
--- /dev/null
+++ b/src/assets/icons/star-icon.tsx
@@ -0,0 +1,23 @@
+type StarProps = {
+ width?: string;
+ height?: string;
+ color?: string;
+};
+function StarIcon({ width, height, color }: StarProps) {
+ return (
+
+ );
+}
+
+export default StarIcon;
diff --git a/src/assets/images/X.svg b/src/assets/images/X.svg
new file mode 100644
index 0000000..25ed361
--- /dev/null
+++ b/src/assets/images/X.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/add.svg b/src/assets/images/add.svg
new file mode 100644
index 0000000..fc4350c
--- /dev/null
+++ b/src/assets/images/add.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/camera.svg b/src/assets/images/camera.svg
new file mode 100644
index 0000000..0cd4f68
--- /dev/null
+++ b/src/assets/images/camera.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/checked.svg b/src/assets/images/checked.svg
new file mode 100644
index 0000000..72811db
--- /dev/null
+++ b/src/assets/images/checked.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/common-rate-character.svg b/src/assets/images/common-rate-character.svg
new file mode 100644
index 0000000..0f4c4f8
--- /dev/null
+++ b/src/assets/images/common-rate-character.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/complete-review.svg b/src/assets/images/complete-review.svg
new file mode 100644
index 0000000..50490bb
--- /dev/null
+++ b/src/assets/images/complete-review.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/dummy-photo.jpeg b/src/assets/images/dummy-photo.jpeg
new file mode 100644
index 0000000..3b38117
Binary files /dev/null and b/src/assets/images/dummy-photo.jpeg differ
diff --git a/src/assets/images/gray-logo.jpeg b/src/assets/images/gray-logo.jpeg
new file mode 100644
index 0000000..85c951e
Binary files /dev/null and b/src/assets/images/gray-logo.jpeg differ
diff --git a/src/assets/images/haru-logo.png b/src/assets/images/haru-logo.png
new file mode 100644
index 0000000..4222a9a
Binary files /dev/null and b/src/assets/images/haru-logo.png differ
diff --git a/src/assets/images/heading.svg b/src/assets/images/heading.svg
new file mode 100644
index 0000000..2eebf07
--- /dev/null
+++ b/src/assets/images/heading.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/high-rate-character.svg b/src/assets/images/high-rate-character.svg
new file mode 100644
index 0000000..7d76928
--- /dev/null
+++ b/src/assets/images/high-rate-character.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/highest-rate-character.svg b/src/assets/images/highest-rate-character.svg
new file mode 100644
index 0000000..0536ece
--- /dev/null
+++ b/src/assets/images/highest-rate-character.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/img_3159.jpg b/src/assets/images/img_3159.jpg
new file mode 100644
index 0000000..5256397
Binary files /dev/null and b/src/assets/images/img_3159.jpg differ
diff --git a/src/assets/images/insaeng-logo.png b/src/assets/images/insaeng-logo.png
new file mode 100644
index 0000000..338bc04
Binary files /dev/null and b/src/assets/images/insaeng-logo.png differ
diff --git a/src/assets/images/kakao.svg b/src/assets/images/kakao.svg
new file mode 100644
index 0000000..6575a07
--- /dev/null
+++ b/src/assets/images/kakao.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/layer.svg b/src/assets/images/layer.svg
new file mode 100644
index 0000000..0a01d41
--- /dev/null
+++ b/src/assets/images/layer.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/login-banner.svg b/src/assets/images/login-banner.svg
new file mode 100644
index 0000000..3b4a1a7
--- /dev/null
+++ b/src/assets/images/login-banner.svg
@@ -0,0 +1,81 @@
+
diff --git a/src/assets/images/lookup-logo.png b/src/assets/images/lookup-logo.png
new file mode 100644
index 0000000..8745d7d
Binary files /dev/null and b/src/assets/images/lookup-logo.png differ
diff --git a/src/assets/images/low-rate-character.svg b/src/assets/images/low-rate-character.svg
new file mode 100644
index 0000000..d8c65e8
--- /dev/null
+++ b/src/assets/images/low-rate-character.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/lowest-rate-character.svg b/src/assets/images/lowest-rate-character.svg
new file mode 100644
index 0000000..50b63d5
--- /dev/null
+++ b/src/assets/images/lowest-rate-character.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/mono-logo.png b/src/assets/images/mono-logo.png
new file mode 100644
index 0000000..8a4003a
Binary files /dev/null and b/src/assets/images/mono-logo.png differ
diff --git a/src/assets/images/no-images.svg b/src/assets/images/no-images.svg
new file mode 100644
index 0000000..9634d14
--- /dev/null
+++ b/src/assets/images/no-images.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/assets/images/oldmoon-logo.jpg b/src/assets/images/oldmoon-logo.jpg
new file mode 100644
index 0000000..3a48a6f
Binary files /dev/null and b/src/assets/images/oldmoon-logo.jpg differ
diff --git a/src/assets/images/photoism-logo.png b/src/assets/images/photoism-logo.png
new file mode 100644
index 0000000..2da82cb
Binary files /dev/null and b/src/assets/images/photoism-logo.png differ
diff --git a/src/assets/images/photomatic-logo.jpg b/src/assets/images/photomatic-logo.jpg
new file mode 100644
index 0000000..de5e2e1
Binary files /dev/null and b/src/assets/images/photomatic-logo.jpg differ
diff --git a/src/assets/images/planb-logo.png b/src/assets/images/planb-logo.png
new file mode 100644
index 0000000..428d931
Binary files /dev/null and b/src/assets/images/planb-logo.png differ
diff --git a/src/assets/images/point.svg b/src/assets/images/point.svg
new file mode 100644
index 0000000..abac46c
--- /dev/null
+++ b/src/assets/images/point.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/polygon.svg b/src/assets/images/polygon.svg
new file mode 100644
index 0000000..37c3747
--- /dev/null
+++ b/src/assets/images/polygon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/review_tags/clean-photo.svg b/src/assets/images/review_tags/clean-photo.svg
new file mode 100644
index 0000000..7bae441
--- /dev/null
+++ b/src/assets/images/review_tags/clean-photo.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/review_tags/clean_booth.svg b/src/assets/images/review_tags/clean_booth.svg
new file mode 100644
index 0000000..6ca22ba
--- /dev/null
+++ b/src/assets/images/review_tags/clean_booth.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/review_tags/dark-photo.svg b/src/assets/images/review_tags/dark-photo.svg
new file mode 100644
index 0000000..cf8520c
--- /dev/null
+++ b/src/assets/images/review_tags/dark-photo.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/images/review_tags/large_booth.svg b/src/assets/images/review_tags/large_booth.svg
new file mode 100644
index 0000000..7f123bd
--- /dev/null
+++ b/src/assets/images/review_tags/large_booth.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/review_tags/large_wait.svg b/src/assets/images/review_tags/large_wait.svg
new file mode 100644
index 0000000..4831b9e
--- /dev/null
+++ b/src/assets/images/review_tags/large_wait.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/review_tags/light-photo.svg b/src/assets/images/review_tags/light-photo.svg
new file mode 100644
index 0000000..7bd1c97
--- /dev/null
+++ b/src/assets/images/review_tags/light-photo.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/review_tags/multiple_bg.svg b/src/assets/images/review_tags/multiple_bg.svg
new file mode 100644
index 0000000..acca2b5
--- /dev/null
+++ b/src/assets/images/review_tags/multiple_bg.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/review_tags/multiple_frame.svg b/src/assets/images/review_tags/multiple_frame.svg
new file mode 100644
index 0000000..b788d30
--- /dev/null
+++ b/src/assets/images/review_tags/multiple_frame.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/assets/images/review_tags/natural.svg b/src/assets/images/review_tags/natural.svg
new file mode 100644
index 0000000..9f23961
--- /dev/null
+++ b/src/assets/images/review_tags/natural.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/review_tags/no-light.svg b/src/assets/images/review_tags/no-light.svg
new file mode 100644
index 0000000..88aec2b
--- /dev/null
+++ b/src/assets/images/review_tags/no-light.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/review_tags/odd.svg b/src/assets/images/review_tags/odd.svg
new file mode 100644
index 0000000..fa43bdb
--- /dev/null
+++ b/src/assets/images/review_tags/odd.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/review_tags/powder.svg b/src/assets/images/review_tags/powder.svg
new file mode 100644
index 0000000..6fb1bb7
--- /dev/null
+++ b/src/assets/images/review_tags/powder.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/review_tags/selfie.svg b/src/assets/images/review_tags/selfie.svg
new file mode 100644
index 0000000..a84502f
--- /dev/null
+++ b/src/assets/images/review_tags/selfie.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/review_tags/tool.svg b/src/assets/images/review_tags/tool.svg
new file mode 100644
index 0000000..cc521cf
--- /dev/null
+++ b/src/assets/images/review_tags/tool.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/search.svg b/src/assets/images/search.svg
new file mode 100644
index 0000000..d8e755a
--- /dev/null
+++ b/src/assets/images/search.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/src/assets/images/share-complete.png b/src/assets/images/share-complete.png
new file mode 100644
index 0000000..d797c97
Binary files /dev/null and b/src/assets/images/share-complete.png differ
diff --git a/src/assets/images/share-logo.svg b/src/assets/images/share-logo.svg
new file mode 100644
index 0000000..f913977
--- /dev/null
+++ b/src/assets/images/share-logo.svg
@@ -0,0 +1,118 @@
+
\ No newline at end of file
diff --git a/src/assets/images/signature-logo.jpg b/src/assets/images/signature-logo.jpg
new file mode 100644
index 0000000..2074445
Binary files /dev/null and b/src/assets/images/signature-logo.jpg differ
diff --git a/src/components/Album/DateModal.tsx b/src/components/Album/DateModal.tsx
new file mode 100644
index 0000000..9ea1c3d
--- /dev/null
+++ b/src/components/Album/DateModal.tsx
@@ -0,0 +1,73 @@
+import styled from "styled-components";
+import tw from "twin.macro";
+import X from "../../assets/images/X.svg?react";
+import { useState } from "react";
+
+type ModalProps = {
+ closeModal: () => void;
+ setDate: (tags: string) => void;
+}
+
+export default function DateModal({ closeModal, setDate }: ModalProps) {
+ const [year, setYear] = useState("");
+ const [month, setMonth] = useState("");
+
+ const confirm = () => {
+ setDate(year + "년 " + month + "월");
+ closeModal();
+ };
+
+ return (
+
+
+ 날짜 입력
+ 사진을 찍은 날짜를 선택해주세요!
+
+
+
+
+
+
setYear(e.target.value)} />
+ 년
+ setMonth(e.target.value)} />
+ 월
+
+
+ 확인
+
+
+ );
+}
+
+const ModalOverlay = styled.div`
+ ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`}
+ background-color: rgba(23, 28, 36, 0.8);
+`;
+
+const ModalContent = styled.div`
+ ${tw`w-[390px] h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center`}
+`;
+
+const Title = styled.h2`
+ ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`}
+`;
+
+const SubText = styled.p`
+ ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`}
+`;
+
+const CloseButton = styled.button`
+ ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`}
+`;
+
+const DateContainer = styled.div`
+ ${tw`space-y-4 my-4 flex flex-col items-start`}
+`;
+
+const ConfirmButton = styled.button`
+ ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`}
+`;
+
+const StyledInput = styled.input`
+ ${tw`w-full h-full rounded-lg text-center text-gray400 bg-gray100`}
+`;
\ No newline at end of file
diff --git a/src/components/Album/DeleteModal.tsx b/src/components/Album/DeleteModal.tsx
new file mode 100644
index 0000000..24cb573
--- /dev/null
+++ b/src/components/Album/DeleteModal.tsx
@@ -0,0 +1,59 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+
+type ModalType = {
+ sub?: string;
+ title: string;
+ option: string[];
+ onClick?: () => void;
+ onLeftOptionClick: () => void;
+ onRightOptionClick: () => void;
+};
+
+export default function DeleteModal({ sub, title, option, onClick, onLeftOptionClick, onRightOptionClick }: ModalType) {
+ return (
+
+
+ {sub ? sub : ""}
+ {title}
+
+
+
+
+
+
+ );
+}
+
+const Overlay = styled.div`
+ ${tw`
+ w-full h-full bg-[black] bg-opacity-40
+ fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%]
+ flex justify-center items-center
+ z-[20]
+ `}
+`;
+const Container = styled.div`
+ ${tw`w-[95] h-[170px] flex flex-col bg-gray100 font-display p-[20px] rounded-[8px]`}
+
+ .sub {
+ ${tw`font-normal text-[12px] text-gray400`}
+ }
+ .title {
+ ${tw`font-medium text-[18px] text-gray700 mt-1`}
+ }
+ .left-btn {
+ ${tw`w-1/2 h-[50px] rounded-[8px] font-display font-semibold text-[16px] bg-main text-[#FFFFFF]`}
+ }
+ .right-btn {
+ ${tw`w-1/2 h-[50px] rounded-[8px] font-display font-semibold text-[16px] bg-[#FFFFFF] text-gray400`}
+ }
+`;
+
+const RowBox = styled.div`
+ ${tw`w-full flex flex-row items-center gap-[10px] mt-[30px]`}
+`;
diff --git a/src/components/Album/Footer.tsx b/src/components/Album/Footer.tsx
new file mode 100644
index 0000000..e030689
--- /dev/null
+++ b/src/components/Album/Footer.tsx
@@ -0,0 +1,46 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import {useState} from "react";
+import AlbumLikeIcon from "../../assets/icons/album-like-icon.tsx";
+import AlbumDeleteIcon from "../../assets/icons/album-delete-icon.tsx";
+
+export default function Footer() {
+ const [status, setStatus] = useState('initial');
+
+ const isActive = (status: boolean) => {
+ if (status) {
+ return "#5453EE";
+ } else {
+ return "#C7C9CE";
+ }
+ };
+
+ const isLiking = status === 'liking';
+ const isDeleting = status === 'deleting';
+
+ return (
+
+ setStatus('liking')} disabled={isLiking}>
+
+ 좋아요
+
+ setStatus('deleting')} disabled={isDeleting}>
+
+ 삭제
+
+
+ );
+}
+
+const Container = styled.nav`
+ ${tw`fixed bottom-0 flex flex-row [max-width: 480px] w-full h-[60px] px-[80px] items-center justify-between bg-background z-30`}
+`;
+
+const MenuBtn = styled.button`
+ ${tw`flex flex-col items-center w-[25px]`}
+`;
+
+const Text = styled.span<{ $active: boolean }>`
+ ${tw`font-display font-medium text-[9px] mt-0.5`}
+ ${(props) => (props.$active ? tw`text-main` : tw`text-gray200`)}
+`;
diff --git a/src/components/Album/ImageCard.tsx b/src/components/Album/ImageCard.tsx
new file mode 100644
index 0000000..c4668c1
--- /dev/null
+++ b/src/components/Album/ImageCard.tsx
@@ -0,0 +1,38 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import DummyImg from "../../assets/images/dummy-photo.jpeg";
+import LikeFilledIcon from "../../assets/icons/like-filled-icon";
+import { useState } from "react";
+import LikeNotFilledIcon from "../../assets/icons/like-not-filled-icon";
+
+type ImageCardProps = {
+ isEditing: boolean;
+}
+
+function ImageCard({ isEditing }: ImageCardProps) {
+ const [like, setLike] = useState(false);
+
+ return (
+
+ {!isEditing && (
+ setLike(!like)}>
+ {like ? : }
+
+ )}
+
+ );
+}
+
+export default ImageCard;
+
+const ImgBox = styled.div<{ $imageurl: string }>`
+ ${tw`w-full [aspect-ratio: 3 / 4] relative rounded-[8px] `}
+
+ background-image: url(${(props) => props.$imageurl});
+ background-size: cover;
+ background-position: center;
+`;
+
+const LikeBtn = styled.button`
+ ${tw`absolute w-[30px] h-[30px] rounded-full bg-[white] flex items-center justify-center bottom-1.5 right-1.5`}
+`;
diff --git a/src/components/Booth/BoothInfo.tsx b/src/components/Booth/BoothInfo.tsx
new file mode 100644
index 0000000..37389fc
--- /dev/null
+++ b/src/components/Booth/BoothInfo.tsx
@@ -0,0 +1,49 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import LikeIcon from "../../assets/icons/like-filled-icon";
+import { getDistance } from "../../hooks/getLocation";
+import useBoothFilterStore from "../../store/useBoothFilterStore";
+import { SpecificBoothInfo } from "../../@types/booth";
+function BoothInfoSection({ name, road, x, y }: SpecificBoothInfo) {
+ const { lat, lng } = useBoothFilterStore();
+
+ return (
+
+
+ {name}
+ {road}
+ {`현재 위치로 ${getDistance(x, y, lat, lng)}`}
+
+
+
+
+
+
+
+ );
+}
+
+export default BoothInfoSection;
+
+const Container = styled.div`
+ ${tw`w-full px-[16px] flex flex-row font-display justify-between items-start`}
+
+ .main-text {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+ .sub-text {
+ ${tw`font-normal text-[12px] text-gray400`}
+ }
+ .guide-btn {
+ ${tw`px-[15px] h-[39px] rounded-[30px] bg-main font-semibold text-[16px] text-[#FFFFFF]`}
+ }
+`;
+
+const ColBox = styled.div`
+ ${tw`flex flex-col`}
+`;
diff --git a/src/components/Booth/BoothRating.tsx b/src/components/Booth/BoothRating.tsx
new file mode 100644
index 0000000..0a20d63
--- /dev/null
+++ b/src/components/Booth/BoothRating.tsx
@@ -0,0 +1,72 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import Rating from "../Common/Rating";
+import Polygon from "../../assets/images/polygon.svg?react";
+import HighestSvg from "../../assets/images/highest-rate-character.svg?react";
+import HighSvg from "../../assets/images/high-rate-character.svg?react";
+import CommonSvg from "../../assets/images/common-rate-character.svg?react";
+import LowSvg from "../../assets/images/low-rate-character.svg";
+import LowestSvg from "../../assets/images/lowest-rate-character.svg";
+import { useParams } from "react-router-dom";
+import { useAuthStore } from "../../store/useAuthStore";
+import { useQuery } from "@tanstack/react-query";
+import { getRating } from "../../api/review";
+function BoothRating() {
+ const { boothId } = useParams() as { boothId: string };
+ const { accessToken } = useAuthStore();
+ const { isLoading, data: score } = useQuery({
+ queryKey: ["getRate", boothId],
+ queryFn: () => getRating(boothId, accessToken!),
+ });
+
+ const RenderCharacter = () => {
+ if (score! <= 1) return ;
+ if (score! <= 2) return ;
+ if (score! <= 3) return ;
+ if (score! <= 4) return ;
+ if (score! <= 5) return ;
+ };
+
+ const getRatingText = () => {
+ if (score! <= 1) return "아쉬워요";
+ if (score! <= 2) return "조금 아쉬워요";
+ if (score! <= 3) return "무난해요";
+ if (score! <= 4) return "만족해요";
+ if (score! <= 5) return "완전만족해요";
+ };
+
+ return (
+
+ 부스 만족도
+
+
+
+ {getRatingText()}
+
+
+ {score &&
}
+
+
+ );
+}
+
+export default BoothRating;
+
+const Wrapper = styled.div`
+ ${tw`w-full h-[175px] p-[18px] flex flex-col font-display`}
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+`;
+
+const RatingBox = styled.div`
+ ${tw`relative flex flex-col items-center justify-center min-w-[248px] w-[75%] h-[83px] rounded-[9px] bg-main mt-[10px] mr-1`}
+ .sub-text {
+ ${tw`font-normal text-[16px] text-[#FFFFFF] mt-[5px]`}
+ }
+ .polygon {
+ ${tw`absolute left-[98%] top-4 `}
+ }
+`;
diff --git a/src/components/Booth/FeedImgList.tsx b/src/components/Booth/FeedImgList.tsx
new file mode 100644
index 0000000..32d6a5d
--- /dev/null
+++ b/src/components/Booth/FeedImgList.tsx
@@ -0,0 +1,57 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import RightArrowIcon from "../../assets/icons/right-arrow";
+
+function FeedImgList() {
+ return (
+
+
+
+ 사진
+ 60
+
+
+ 더보기
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default FeedImgList;
+
+const Container = styled.div`
+ ${tw`flex flex-col w-full px-[18px] py-[30px]`}
+`;
+
+const RowBox = styled.div`
+ ${tw`flex flex-row items-center justify-between font-display `}
+
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+ .count {
+ ${tw`font-semibold text-[18px] text-gray400`}
+ }
+`;
+
+const MoreBtn = styled.button`
+ ${tw`flex w-[70px] h-[26px] rounded-[30px] bg-gray200 font-normal text-[13px] text-[#FFFFFF] items-center justify-center gap-1`}
+`;
+
+const ImgBox = styled.div`
+ ${tw`w-full grid gap-[10px] mt-[25px]`}
+ grid-template-columns: repeat(3, 1fr);
+
+ .img-container {
+ ${tw`w-full [aspect-ratio: 1 / 1] rounded-[8px] bg-gray200`}
+ }
+`;
diff --git a/src/components/Booth/FeedTagSection.tsx b/src/components/Booth/FeedTagSection.tsx
new file mode 100644
index 0000000..29c8258
--- /dev/null
+++ b/src/components/Booth/FeedTagSection.tsx
@@ -0,0 +1,59 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import { getReviewBoothTagImgUrl, getReviewPhotoTagImgUrl } from "../../hooks/getImageUrl";
+import { TagCnt } from "../../@types/review";
+
+type TagSectionProps = {
+ title: string;
+ category: string;
+ data: TagCnt[];
+};
+function FeedTagSection({ title, category, data }: TagSectionProps) {
+ return (
+
+ {title}
+ {data.map((d, index: number) => (
+
+
+
+
+
{d.featureName}
+
+ {d.count}
+
+
+ ))}
+
+ );
+}
+
+export default FeedTagSection;
+const Container = styled.div`
+ ${tw`flex flex-col w-full px-[18px] py-[30px]`}
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700 mb-2`}
+ }
+`;
+
+const TagWrapper = styled.div`
+ ${tw`relative w-full h-[42px] rounded-[8px] bg-gray200 my-[4px]`}
+`;
+
+const Content = styled.div`
+ ${tw`flex items-center mx-[13px] font-display justify-between h-full`}
+
+ img {
+ ${tw`mr-2`}
+ }
+ .tag-label {
+ ${tw`font-semibold text-[16px] text-[#FFFFFF]`}
+ }
+ .cnt {
+ ${tw`font-semibold text-[14px] text-[#FFFFFF] `}
+ }
+`;
diff --git a/src/components/Booth/ImgSlider.tsx b/src/components/Booth/ImgSlider.tsx
new file mode 100644
index 0000000..5ee00cc
--- /dev/null
+++ b/src/components/Booth/ImgSlider.tsx
@@ -0,0 +1,70 @@
+import Slider from "react-slick";
+import "slick-carousel/slick/slick.css";
+import "slick-carousel/slick/slick-theme.css";
+import tw from "twin.macro";
+import styled from "styled-components";
+import { getLogoUrl } from "../../hooks/getImageUrl";
+import { getRecentImages } from "../../api/review";
+import { useParams } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { useAuthStore } from "../../store/useAuthStore";
+
+function ImgSlider({ type }: { type: string }) {
+ const { boothId } = useParams() as { boothId: string };
+ const { accessToken } = useAuthStore();
+ const { isLoading, data: images } = useQuery({
+ queryKey: ["getRecentImages", boothId],
+ queryFn: () => getRecentImages(boothId, accessToken!),
+ });
+ const settings = {
+ dots: true,
+ infinite: false,
+ speed: 500,
+ slidesToShow: 1,
+ slidesToScroll: 1,
+ arrows: false,
+ appendDots: (dots: any) => (
+
+ ),
+ dotsClass: "dots_custom",
+ };
+
+ if (!isLoading && images && images.filePaths.length > 0) {
+ return (
+
+
+ {images.filePaths.map((image, index) => (
+
+ ))}
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+}
+
+export default ImgSlider;
+
+const Container = styled.div`
+ ${tw`w-full p-[16px]`}
+`;
+
+const ImgBox = styled.img`
+ ${tw`w-full [aspect-ratio: 1 / 1] rounded-[8px]`}
+`;
diff --git a/src/components/Booth/ReviewItem.tsx b/src/components/Booth/ReviewItem.tsx
new file mode 100644
index 0000000..d47aa0b
--- /dev/null
+++ b/src/components/Booth/ReviewItem.tsx
@@ -0,0 +1,71 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import { Review } from "../../@types/review";
+
+function ReviewItem({ name, year, month, date, contents, features, imageUrl, imagesCount }: Review) {
+ return (
+
+
+
+
+
+
+ {`@${name}`}
+ {`${year}.${month}.${date} 작성`}
+
+
+
{contents}
+
+ {imagesCount > 0 && (
+
+ {imagesCount - 1 > 0 && {`+${imagesCount - 1}`}
}
+
+ )}
+
+ {features?.length > 0 && (
+
+ {features.slice(0, 2).map((feature, index) => (
+ {feature}
+ ))}
+ {features.length > 2 && {`+${features.length - 2}`}}
+
+ )}
+
+ );
+}
+
+export default ReviewItem;
+
+const Container = styled.div`
+ ${tw`w-full flex flex-col font-display my-[20px] gap-[10px]`}
+ .profile-icon {
+ ${tw`w-[33px] h-[33px] rounded-[50%] bg-gray200`}
+ }
+ .nickname {
+ ${tw`font-semibold text-[16px] text-gray700`}
+ }
+ .date {
+ ${tw`font-normal text-[10px] text-gray400`}
+ }
+ .review-text {
+ ${tw`font-normal text-[14px] text-gray600`}
+ }
+`;
+
+const ImgBox = styled.div<{ $imageurl: string }>`
+ ${tw`w-[94px] h-[94px] relative rounded-[4px] `}
+
+ background-image: url(${(props) => props.$imageurl});
+ background-size: cover;
+ background-position: center;
+ .num-tag {
+ ${tw`absolute w-[33px] h-[22px] rounded-[24px] bg-[#000000] text-[#FFFFFF] font-normal text-[10px] flex items-center justify-center right-1 bottom-1`}
+ }
+`;
+const TagList = styled.div`
+ ${tw`flex gap-[5px] items-center`}
+`;
+
+const TagBox = styled.span`
+ ${tw`flex items-center h-[30px] rounded-[24px] bg-gray100 font-normal text-[12px] text-gray400 px-[20px]`}
+`;
diff --git a/src/components/Booth/ReviewListSection.tsx b/src/components/Booth/ReviewListSection.tsx
new file mode 100644
index 0000000..cc0a93d
--- /dev/null
+++ b/src/components/Booth/ReviewListSection.tsx
@@ -0,0 +1,45 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import ReviewItem from "./ReviewItem";
+
+import { Review } from "../../@types/review";
+function ReviewListSection({
+ data,
+}: {
+ data: {
+ reviewCount: number;
+ reviews: Review[];
+ };
+}) {
+ return (
+
+
+ 리뷰 {}
+
+ {data.reviews.length > 0 &&
+ data.reviews.map((review, index) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default ReviewListSection;
+const Container = styled.div`
+ ${tw`flex flex-col w-full px-[18px] py-[30px]`}
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700 mb-2`}
+ }
+`;
diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx
new file mode 100644
index 0000000..5e99723
--- /dev/null
+++ b/src/components/Common/Header.tsx
@@ -0,0 +1,37 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import BackIcon from "../../assets/icons/back-icon";
+type HeaderProps = {
+ mainText: string;
+ handleBackClick: () => void;
+ subText?: string;
+};
+function Header({ mainText, subText, handleBackClick }: HeaderProps) {
+ return (
+
+
+
+
+
+ {mainText}
+ {subText}
+
+
+ );
+}
+
+export default Header;
+const Container = styled.header`
+ ${tw`w-full [max-width: 480px] py-[13px] fixed z-10 top-0 border-b-2 border-b-gray100 flex items-center justify-center bg-[#FFFFFF]`}
+
+ .back-icon {
+ ${tw`absolute left-[10px]`}
+ }
+
+ .booth-title {
+ ${tw`font-display font-semibold text-[22px] text-gray700`}
+ }
+ .sub-text {
+ ${tw`font-medium text-[12px] text-gray400`}
+ }
+`;
diff --git a/src/components/Common/MapContainer.tsx b/src/components/Common/MapContainer.tsx
new file mode 100644
index 0000000..1f1a368
--- /dev/null
+++ b/src/components/Common/MapContainer.tsx
@@ -0,0 +1,93 @@
+import { useEffect, useState } from "react";
+import { Map, MapMarker } from "react-kakao-maps-sdk";
+import { getCurrentLocation, trackCurrentPosition } from "../../hooks/getLocation";
+import PointSVG from "../../assets/images/point.svg?url";
+import HeadingSVG from "../../assets/images/heading.svg?url";
+type MapContainerProps = {
+ lat: number;
+ lng: number;
+ children?: React.ReactNode;
+};
+
+function MapContainer({ lat, lng, children }: MapContainerProps) {
+ const [position, setPosition] = useState<{ lat: number; lng: number; heading: number | null }>({
+ lat: 0,
+ lng: 0,
+ heading: null,
+ });
+ useEffect(() => {
+ // 사용자 위치 및 방향을 추적하고 업데이트하는 콜백 함수
+ const stopTracking = trackCurrentPosition(setPosition);
+
+ return () => {
+ // 컴포넌트가 언마운트될 때 위치 추적을 멈춤
+ stopTracking();
+ };
+ }, []);
+
+ // useEffect(() => {
+ // //현 위치 받아오기
+ // const fetchLocation = async () => {
+ // if (lat && lng) {
+ // // lat과 lng이 props로 전달되면 해당 값 사용
+ // setGeo({ lat, lng });
+ // } else {
+ // // 전달되지 않으면 현재 위치 가져오기
+ // const res = await getCurrentLocation();
+ // if (res) {
+ // setGeo(res);
+ // }
+ // }
+ // };
+
+ // fetchLocation();
+ // }, [lat, lng]);
+
+ return (
+
+ );
+}
+
+export default MapContainer;
diff --git a/src/components/Common/Modal.tsx b/src/components/Common/Modal.tsx
new file mode 100644
index 0000000..5c4af2e
--- /dev/null
+++ b/src/components/Common/Modal.tsx
@@ -0,0 +1,60 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+
+type ModalType = {
+ sub?: string;
+ title: string;
+ option: string[];
+ onClick?: () => void;
+ onLeftOptionClick: () => void;
+ onRightOptionClick: () => void;
+};
+
+function Modal({ sub, title, option, onClick, onLeftOptionClick, onRightOptionClick }: ModalType) {
+ return (
+
+
+ {sub ? sub : ""}
+ {title}
+
+
+
+
+
+
+ );
+}
+
+export default Modal;
+const Overlay = styled.div`
+ ${tw`
+ w-full h-full bg-[black] bg-opacity-40
+ fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%]
+ flex justify-center items-center
+ z-[20]
+ `}
+`;
+const Container = styled.div`
+ ${tw`w-[95] h-[170px] flex flex-col bg-gray100 font-display p-[20px] rounded-[8px]`}
+
+ .sub {
+ ${tw`font-normal text-[12px] text-gray400`}
+ }
+ .title {
+ ${tw`font-medium text-[18px] text-gray700 mt-1`}
+ }
+ .left-btn {
+ ${tw`w-1/2 h-[50px] rounded-[8px] font-display font-semibold text-[16px] bg-main text-[#FFFFFF]`}
+ }
+ .right-btn {
+ ${tw`w-1/2 h-[50px] rounded-[8px] font-display font-semibold text-[16px] bg-[#FFFFFF] text-gray400`}
+ }
+`;
+
+const RowBox = styled.div`
+ ${tw`w-full flex flex-row items-center gap-[10px] mt-[30px]`}
+`;
diff --git a/src/components/Common/NavBar.tsx b/src/components/Common/NavBar.tsx
index 26d8da5..707a70e 100644
--- a/src/components/Common/NavBar.tsx
+++ b/src/components/Common/NavBar.tsx
@@ -10,7 +10,8 @@ function NavBar() {
const navigate = useNavigate();
const isActive = (menu: string) => {
- if (locationNow.pathname === menu) {
+ // 하위 경로도 포함하여 활성화 확인
+ if (locationNow.pathname.startsWith(menu)) {
return "#5453EE";
} else {
return "#C7C9CE";
@@ -21,15 +22,15 @@ function NavBar() {
navigate("/home")} disabled={locationNow.pathname === "/home"}>
- 홈
+ 홈
navigate("/album")} disabled={locationNow.pathname === "/album"}>
- 앨범
+ 앨범
- navigate("/my")} disabled={locationNow.pathname === "/my"}>
+ navigate("/my/booth-records")} disabled={locationNow.pathname === "/my"}>
- MY
+ MY
);
@@ -38,14 +39,14 @@ function NavBar() {
export default NavBar;
const Container = styled.nav`
- ${tw`fixed bottom-0 flex flex-row [max-width: 480px] w-full h-[60px] px-[53px] items-center justify-between bg-background`}
+ ${tw`fixed bottom-0 flex flex-row [max-width: 480px] w-full h-[60px] px-[53px] items-center justify-between bg-background z-30`}
`;
const MenuBtn = styled.button`
${tw`flex flex-col items-center w-[25px]`}
`;
-const Text = styled.span<{ active: boolean }>`
+const Text = styled.span<{ $active: boolean }>`
${tw`font-display font-medium text-[9px] mt-0.5`}
- ${(props) => (props.active ? tw`text-main` : tw`text-gray200`)}
+ ${(props) => (props.$active ? tw`text-main` : tw`text-gray200`)}
`;
diff --git a/src/components/Common/NextButton.tsx b/src/components/Common/NextButton.tsx
new file mode 100644
index 0000000..29167f2
--- /dev/null
+++ b/src/components/Common/NextButton.tsx
@@ -0,0 +1,23 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+type ButtonProps = {
+ text: string;
+ onClick: () => void;
+ disabled?: boolean;
+};
+
+function NextButton({ text, onClick, disabled }: ButtonProps) {
+ return (
+
+ {text}
+
+ );
+}
+
+export default NextButton;
+const Container = styled.button`
+ ${tw`w-[280px] h-[60px] bg-main rounded-lg mt-12 flex justify-center items-center font-semibold text-[20px] text-[#FFFFFF] mx-auto`}
+ &:disabled {
+ ${tw`bg-gray400`}
+ }
+`;
diff --git a/src/components/Common/Rating.tsx b/src/components/Common/Rating.tsx
new file mode 100644
index 0000000..6584b12
--- /dev/null
+++ b/src/components/Common/Rating.tsx
@@ -0,0 +1,54 @@
+import React, { useState } from "react";
+import tw from "twin.macro";
+import styled from "styled-components";
+import { FaStar } from "react-icons/fa6";
+
+type RatingProps = {
+ w: string;
+ h: string;
+ readonly: boolean;
+ rate?: number; // Accept ratings with decimals
+ setRate?: React.Dispatch>;
+};
+
+function Rating({ w, h, readonly, rate, setRate }: RatingProps) {
+ // const [rating, setRating] = useState(rate || 0);
+ const handleClickStar = (index: number) => {
+ if (!readonly && setRate) {
+ setRate(index + 1);
+ }
+ };
+ const calculateRate = (rate: number, index: number) => {
+ if (rate >= index) {
+ return "100%";
+ }
+ if (Math.floor(index - rate) > 0) {
+ return "0%";
+ }
+ const percentage = ((rate % 1) * 100).toFixed();
+ return `${percentage}%`;
+ };
+ return (
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ handleClickStar(index)}
+ className={` ${!readonly && rate! >= index + 1 ? "text-yellow" : "text-purple"}`}
+ />
+ {readonly && (
+
+
+
+ )}
+
+ ))}
+
+ );
+}
+
+export default Rating;
diff --git a/src/components/Home/AddBtn.tsx b/src/components/Home/AddBtn.tsx
new file mode 100644
index 0000000..205d883
--- /dev/null
+++ b/src/components/Home/AddBtn.tsx
@@ -0,0 +1,50 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import AddSvg from "../../assets/images/add.svg?react";
+import { useNavigate } from "react-router-dom";
+import { useState } from "react";
+import Modal from "../Common/Modal";
+import { useAuthStore } from "../../store/useAuthStore";
+function AddBtn() {
+ const navigate = useNavigate();
+ const { isLoggedIn } = useAuthStore();
+ const [isOpen, setIsOpen] = useState(false);
+ const goToPhotoUpload = () => {
+ if (isLoggedIn) {
+ navigate("/photo-upload");
+ } else {
+ setIsOpen(true);
+ }
+ };
+ return (
+ <>
+
+
+ 추억 저장하기
+
+ {isOpen && (
+ //로그인 모달창
+ setIsOpen(false)}
+ onLeftOptionClick={() => navigate("/login")}
+ onRightOptionClick={() => setIsOpen(false)}
+ />
+ )}
+ >
+ );
+}
+
+export default AddBtn;
+
+const Container = styled.button`
+ ${tw`absolute bottom-11 z-10 w-[280px] h-[57px] flex flex-row items-center justify-center bg-main rounded-[8px] `}
+ left: 50%;
+ transform: translateX(-50%);
+
+ span {
+ ${tw`ml-2 font-display font-semibold text-[22px] text-[#FFFFFF]`}
+ }
+`;
diff --git a/src/components/Home/BoothMap.tsx b/src/components/Home/BoothMap.tsx
new file mode 100644
index 0000000..3527b12
--- /dev/null
+++ b/src/components/Home/BoothMap.tsx
@@ -0,0 +1,73 @@
+import MapContainer from "../Common/MapContainer";
+import { CustomOverlayMap } from "react-kakao-maps-sdk";
+import CustomMarker from "../../assets/icons/booth-marker";
+import { useEffect, useState } from "react";
+import ActiveCustomMarker from "../../assets/icons/active-booth-marker";
+import BoothModal from "./BoothModal";
+import { getLogoUrl } from "../../hooks/getImageUrl";
+import { getCurrentLocation } from "../../hooks/getLocation";
+import { useQuery } from "@tanstack/react-query";
+import useBoothFilterStore from "../../store/useBoothFilterStore";
+import { getBoothLatLng } from "../../api/booth";
+import { useAuthStore } from "../../store/useAuthStore";
+
+function BoothMap() {
+ const { lat, lng, selectedBrands } = useBoothFilterStore();
+ const [activeId, setActiveId] = useState(-1);
+
+ const { accessToken } = useAuthStore();
+
+ //전체 포토부스 위치 정보 조회 api 호출
+ const { isLoading, data } = useQuery({
+ queryKey: ["getBoothLatLng", lat, lng, selectedBrands],
+ queryFn: () => getBoothLatLng(lat, lng, selectedBrands!, accessToken!),
+ });
+
+ //처음 페이지 접속 시 사용자의 현 위치를 받아와서 center 세팅
+ useEffect(() => {
+ //현 위치 받아오기
+ const fetchLocation = async () => {
+ const res = await getCurrentLocation();
+ if (res) {
+ useBoothFilterStore.setState({
+ lat: res.lat,
+ lng: res.lng,
+ });
+ }
+ };
+
+ fetchLocation();
+ }, []);
+
+ const handleClick = (id: number) => {
+ if (activeId === id) {
+ setActiveId(-1);
+ } else {
+ setActiveId(id);
+ }
+ };
+
+ return (
+
+ {/* CustomOverlayMap으로 커스텀 마커를 직접 렌더링 */}
+ {!isLoading &&
+ data &&
+ data.map((item, index) => (
+
+ handleClick(item.id)} className="flex flex-col items-center">
+ {activeId === item.id ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+ {/* 클릭 시 해당 포토부스에 해당 되는 모달창 렌더링 */}
+ {activeId >= 0 && }
+
+ );
+}
+
+export default BoothMap;
diff --git a/src/components/Home/BoothModal.tsx b/src/components/Home/BoothModal.tsx
new file mode 100644
index 0000000..9d0d190
--- /dev/null
+++ b/src/components/Home/BoothModal.tsx
@@ -0,0 +1,126 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import LayerBar from "../../assets/images/layer.svg?react";
+import StarIcon from "../../assets/icons/star-icon";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { getBoothInfo } from "../../api/booth";
+import { getLogoUrl } from "../../hooks/getImageUrl";
+import useBoothFilterStore from "../../store/useBoothFilterStore";
+import { getDistance } from "../../hooks/getLocation";
+import { useAuthStore } from "../../store/useAuthStore";
+function BoothModal({ boothId }: { boothId: string }) {
+ const [currentY, setCurrentY] = useState(0);
+ const navigate = useNavigate();
+
+ const { lat, lng } = useBoothFilterStore();
+ // 모달 열리는 애니메이션 실행
+ useEffect(() => {
+ document.body.style.overflow = "hidden"; // 스크롤 비활성화
+ setCurrentY(0); // 처음에 열릴 때 0으로 설정하여 애니메이션 실행
+ return () => {
+ document.body.style.overflow = "auto"; // 모달이 닫힐 때 스크롤 활성화
+ };
+ }, []);
+ const { accessToken } = useAuthStore();
+ //특정 포토부스 정보 조회 api 호출
+ const { isLoading: isBoothInfoLoading, data: boothInfo } = useQuery({
+ queryKey: ["getBoothInfo", boothId],
+ queryFn: () => getBoothInfo(boothId, accessToken!),
+ });
+
+ return (
+ navigate(`/home/${boothId}/feed`)}
+ >
+
+
+ {!isBoothInfoLoading && boothInfo ? (
+
+
+
+ {boothInfo.name}
+
+ 4.5
+
+ {`${getDistance(boothInfo.x, boothInfo.y, lat, lng)} 리뷰 567`}
+
+ 깔끔한 소품
+ 빛번짐 없음
+
+
+
+ +210
+
+
+ ) : (
+ loading..
+ )}
+
+ );
+}
+
+export default BoothModal;
+
+const Wrapper = styled.div`
+ ${tw`absolute w-full h-[180px] z-20 bg-[#ffffff] rounded-t-[26px] drop-shadow-sm bottom-0 flex flex-col items-center p-[18px] font-display cursor-pointer`}
+ transform: translateY(100%);
+ animation: slideUp 0.3s ease-in-out forwards;
+
+ @keyframes slideUp {
+ 0% {
+ transform: translateY(100%);
+ }
+ 100% {
+ transform: translateY(0);
+ }
+ }
+
+ &.close {
+ animation: slideDown 0.3s ease-in-out forwards;
+ }
+
+ @keyframes slideDown {
+ 0% {
+ transform: translateY(0);
+ }
+ 100% {
+ transform: translateY(100%);
+ }
+ }
+`;
+
+const Container = styled.div`
+ ${tw`w-full flex items-center justify-between mt-[35px]`}
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700 mr-2`}
+ }
+ .score {
+ ${tw`font-semibold text-[14px] text-gray600 ml-0.5 mt-1`}
+ }
+ .sub-text {
+ ${tw`font-normal text-[14px] text-gray400`}
+ }
+`;
+const InfoBox = styled.div`
+ ${tw`flex flex-col w-3/4 items-start`}
+`;
+
+const ImgBox = styled.div<{ $imageurl: string }>`
+ ${tw`w-[82px] h-[82px] relative rounded-[4px] `}
+
+ background-image: url(${(props) => props.$imageurl});
+ background-size: cover;
+ background-position: center;
+ .num-tag {
+ ${tw`absolute w-[33px] h-[22px] rounded-[24px] bg-[#000000] text-[#FFFFFF] font-normal text-[10px] flex items-center justify-center right-1 bottom-1`}
+ }
+`;
+
+const TagBox = styled.div`
+ ${tw`w-[96px] h-[30px] rounded-[24px] flex items-center justify-center font-normal text-[12px] text-gray400 bg-gray100 mr-1`}
+`;
diff --git a/src/components/Home/BoothSlide.tsx b/src/components/Home/BoothSlide.tsx
new file mode 100644
index 0000000..ab6d757
--- /dev/null
+++ b/src/components/Home/BoothSlide.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import tw from "twin.macro";
+import styled from "styled-components";
+import { BoothCategories } from "../../data/booth-categories";
+import useBoothFilterStore from "../../store/useBoothFilterStore";
+
+function BoothSlide() {
+ //선택한 포토부스 초기화
+ const { selectedBrands, setSelectedBrands } = useBoothFilterStore();
+
+ //클릭된 부스의 id를 전달받아, 이미 선택된 부스라면 제거, 아니라면 추가하는 로직
+ const handleClick = (boothId: string) => {
+ if (selectedBrands!.includes(boothId)) {
+ //이미 선택된 부스면 배열에서 제거
+ setSelectedBrands(selectedBrands!.filter((id) => id !== boothId));
+ } else {
+ //선택되지 않은 부스면 배열에 추가
+ setSelectedBrands([...selectedBrands!, boothId]);
+ }
+ };
+
+ return (
+
+
+ {BoothCategories.map((booth, index) => (
+ handleClick(booth.id!)}>
+ {booth.label}
+
+ ))}
+
+
+ );
+}
+
+export default BoothSlide;
+
+const Container = styled.div`
+ ${tw`absolute top-20 z-10 flex-row items-center w-full`}
+`;
+
+const SlideWrapper = styled.div`
+ ${tw`flex flex-row gap-2 overflow-x-auto ml-4 `}
+ scroll-snap-type: x mandatory; /* 각 버튼이 스냅되게 설정 */
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ -ms-overflow-style: none; /* IE and 엣지 */
+ scrollbar-width: none; /* 파이어폭스 */
+ -webkit-overflow-scrolling: touch; /* 모바일 환경에서 터치 스크롤 부드럽게 처리 */
+`;
+const BoothBtn = styled.button<{ $active: boolean }>`
+ ${tw`border-[1px] px-[22px] h-[35px] font-display font-medium text-[14px] rounded-[24px] shrink-0`}
+ scroll-snap-align: start;
+ ${(props) =>
+ !props.$active ? tw` border-gray400 bg-[#FFFFFF] text-gray400` : tw`border-main bg-[#E1E0FF] text-main `}
+`;
diff --git a/src/components/Home/Search.tsx b/src/components/Home/Search.tsx
new file mode 100644
index 0000000..cffafb7
--- /dev/null
+++ b/src/components/Home/Search.tsx
@@ -0,0 +1,61 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import SearchIcon from "../../assets/icons/search-icon";
+import axios from "axios";
+import { useState } from "react";
+import useBoothFilterStore from "../../store/useBoothFilterStore";
+
+function Search() {
+ const [address, setAddress] = useState("");
+ const { setLat, setLng } = useBoothFilterStore();
+ //주소를 이용한 좌표 검색
+ const searchAddressLatLng = async () => {
+ try {
+ const res = await axios.get(`https://dapi.kakao.com/v2/local/search/address?query=${address}`, {
+ headers: {
+ Authorization: `KakaoAK ${import.meta.env.VITE_KAKAO_REST_API_KEY}`,
+ },
+ });
+ if (res.data.documents.length > 0) {
+ const data = res.data.documents[0];
+ console.log(data);
+ setLat(data.y);
+ setLng(data.x);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+ return (
+
+
+ setAddress(e.target.value)}
+ />
+
+
+
+ );
+}
+
+export default Search;
+
+const Wrapper = styled.div`
+ ${tw`absolute z-10 top-2.5 w-full flex `}
+`;
+
+const Container = styled.div`
+ ${tw` w-full bg-[#FFFFFF] flex flex-row [box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2)] p-[12px] rounded-[8px] justify-between mx-4`}
+ input {
+ ${tw`w-11/12 font-display bg-[#FFFFFF] font-light text-[14px] text-gray400`}
+
+ &:focus {
+ outline: none;
+ }
+ }
+`;
diff --git a/src/components/My/LikeBoothCard.tsx b/src/components/My/LikeBoothCard.tsx
new file mode 100644
index 0000000..71a3056
--- /dev/null
+++ b/src/components/My/LikeBoothCard.tsx
@@ -0,0 +1,65 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import PlanBUrl from "../../assets/images/planb-logo.png?url";
+import StarIcon from "../../assets/icons/star-icon";
+import LikeFilledIcon from "../../assets/icons/like-filled-icon";
+type CardProps = {
+ width?: string;
+ height?: string;
+};
+function LikeBoothCard({ width, height }: CardProps) {
+ return (
+
+
+
+
+
하루필름 건대입구역점
+
+
+ 4.5
+
+
+
+ # 선명한 화질
+ +3
+
+
+
+
+ );
+}
+
+export default LikeBoothCard;
+
+const CardBox = styled.div<{ width?: string; height?: string }>`
+ ${tw`rounded-[8px] bg-gray100 flex gap-[16px] px-[10px] py-[13.5px]`}
+
+ width: ${({ width }) => (width ? width : "100%")};
+ height: ${({ height }) => (height ? height : "135px")};
+
+ /* width에 비례하여 font-size 조정 */
+ font-size: calc(${({ width }) => (width ? "14px" : "17px")}); /* 예시 비율 */
+
+ .booth-name {
+ ${tw`font-display font-medium text-[1em] text-gray700`}
+ }
+ .rating {
+ ${tw`font-display font-semibold text-[1em] text-gray700 mt-[0.0625em]`}
+ }
+ .hash-tag {
+ ${tw`flex items-center h-[2.4em] rounded-[1.5em] bg-[white] px-[10px] font-display font-normal text-[0.9em] text-gray400`}
+ }
+ .like-btn {
+ ${tw`w-[2.2em] h-[2.2em] rounded-full bg-[white] flex items-center justify-center`}
+ }
+`;
+
+const ImgBox = styled.div<{ $imageurl: string }>`
+ ${tw`w-[50%] [aspect-ratio: 1 / 1] relative rounded-[4px] `}
+
+ background-image: url(${(props) => props.$imageurl});
+ background-size: cover;
+ background-position: center;
+`;
diff --git a/src/components/My/MyReviewCard.tsx b/src/components/My/MyReviewCard.tsx
new file mode 100644
index 0000000..a6017a1
--- /dev/null
+++ b/src/components/My/MyReviewCard.tsx
@@ -0,0 +1,41 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import PlaceIcon from "../../assets/icons/place-icon";
+import StarIcon from "../../assets/icons/star-icon";
+function MyReviewCard() {
+ return (
+
+ 8월 1일
+
+
+ );
+}
+
+export default MyReviewCard;
+
+const Card = styled.div<{ imageUrl?: string }>`
+ ${tw`w-full [aspect-ratio: 1 / 1] rounded-[8px] bg-gray200 flex flex-col p-[10px] font-display justify-between`}
+
+ .date {
+ ${tw`font-normal text-[12px] text-[#FFFFFF] w-full text-end`}
+ }
+ .booth-name {
+ ${tw`font-medium text-[12px] text-[#FFFFFF]`}
+ }
+
+ .rating {
+ ${tw`font-semibold text-[14px] text-[#FFFFFF] mt-0.5`}
+ }
+
+ background-url: ${({ imageUrl }) => (imageUrl ? imageUrl : "none")}
+}
+`;
diff --git a/src/components/My/ProfileSection.tsx b/src/components/My/ProfileSection.tsx
new file mode 100644
index 0000000..bcde4b2
--- /dev/null
+++ b/src/components/My/ProfileSection.tsx
@@ -0,0 +1,30 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import EditIcon from "../../assets/icons/edit-icon";
+function ProfileSection() {
+ return (
+
+
+ 홍길동
+
+
+ );
+}
+
+export default ProfileSection;
+
+const Container = styled.div`
+ ${tw`flex flex-col my-[35px] mx-auto items-center font-display gap-0.5`}
+ .img-wrapper {
+ ${tw`w-[77px] h-[77px] rounded-full bg-purple`}
+ }
+ .nickname {
+ ${tw`font-semibold text-[20px] text-gray700`}
+ }
+ .edit-btn {
+ ${tw`flex items-center justify-center gap-0.5 rounded-[24px] h-[26px] w-[100px] font-normal text-[12px] text-[#FFFFFF] bg-gray200`}
+ }
+`;
diff --git a/src/components/My/VisitedBoothCard.tsx b/src/components/My/VisitedBoothCard.tsx
new file mode 100644
index 0000000..ccd8683
--- /dev/null
+++ b/src/components/My/VisitedBoothCard.tsx
@@ -0,0 +1,55 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import PlanBUrl from "../../assets/images/planb-logo.png?url";
+import EditIcon from "../../assets/icons/edit-icon";
+type CardProps = {
+ width?: string;
+ height?: string;
+};
+function VisitedBoothCard({ width, height }: CardProps) {
+ return (
+
+
+
+
+ 하루필름 건대입구역점
+ 8월 2일 이용
+
+
+
+
+ );
+}
+
+export default VisitedBoothCard;
+const CardBox = styled.div<{ width?: string; height?: string }>`
+ ${tw`rounded-[8px] bg-gray100 flex gap-[16px] px-[10px] py-[13.5px]`}
+
+ width: ${({ width }) => (width ? width : "100%")};
+ height: ${({ height }) => (height ? height : "135px")};
+
+ /* width에 비례하여 font-size 조정 */
+ font-size: calc(${({ width }) => (width ? "14px" : "17px")}); /* 예시 비율 */
+
+ .booth-name {
+ ${tw`font-display font-medium text-[1em] text-gray700`}
+ }
+ .visited-date {
+ ${tw`font-display font-medium text-[0.9em] text-gray400 `}
+ }
+
+ .go-btn {
+ ${tw` flex items-center justify-center gap-[2px] px-[12px] py-[6px] text-[0.9em] text-[white] bg-main rounded-[24px]`}
+ }
+`;
+
+const ImgBox = styled.div<{ $imageurl: string }>`
+ ${tw`w-[50%] [aspect-ratio: 1 / 1] relative rounded-[4px] `}
+
+ background-image: url(${(props) => props.$imageurl});
+ background-size: cover;
+ background-position: center;
+`;
diff --git a/src/components/PhotoCheck/HashModal.tsx b/src/components/PhotoCheck/HashModal.tsx
new file mode 100644
index 0000000..f6c652e
--- /dev/null
+++ b/src/components/PhotoCheck/HashModal.tsx
@@ -0,0 +1,130 @@
+import styled from "styled-components";
+import tw from "twin.macro";
+import X from "../../assets/images/X.svg?react";
+import React, { useEffect, useState } from "react";
+
+type ModalProps = {
+ hashTags: string[];
+ closeModal: () => void;
+ setHashTags: React.Dispatch>;
+}
+
+function HashtagModal({ hashTags, closeModal, setHashTags }: ModalProps) {
+ const [countHash, setCountHash] = useState(0);
+ const [hash1, setHash1] = useState(hashTags[0] ?? "");
+ const [hash2, setHash2] = useState(hashTags[1] ?? "");
+ const [hash3, setHash3] = useState(hashTags[2] ?? "");
+
+ useEffect(() => {
+ // 해시태그가 채워진 것만 카운트
+ const count = [hash1, hash2, hash3].filter((hash) => hash.length > 0).length;
+ setCountHash(count);
+ }, [hash1, hash2, hash3]);
+
+ const confirm = () => {
+ setHashTags([hash1, hash2, hash3]);
+ closeModal();
+ };
+
+ return (
+
+
+ 해시태그 추가
+ 최대 3개까지 #를 추가해볼 수 있어요!
+
+
+
+
+
+ 0}>#
+ 0}>
+ setHash1(e.target.value)}
+ />
+
+
+
+ 0}>#
+ 0}>
+ setHash2(e.target.value)}
+ />
+
+
+
+ 0}>#
+ 0}>
+ setHash3(e.target.value)}
+ />
+
+ ({countHash}/3)
+
+
+ 확인
+
+
+ );
+}
+
+export default HashtagModal;
+
+const ModalOverlay = styled.div`
+ ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`}
+ background-color: rgba(23, 28, 36, 0.8);
+`;
+
+const ModalContent = styled.div`
+ ${tw`w-[390px] h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center`}
+`;
+
+const Title = styled.h2`
+ ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`}
+`;
+
+const SubText = styled.p`
+ ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`}
+`;
+
+const CloseButton = styled.button`
+ ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`}
+`;
+
+const HashtagContainer = styled.div`
+ ${tw`space-y-4 my-4 flex flex-col items-start`}
+`;
+
+type HashtagItemProps = {
+ hasText: boolean;
+}
+
+const HashtagItem = styled.div`
+ ${tw`w-[139px] h-[42px] flex items-center rounded-lg pl-4 text-gray400 text-lg font-medium`}
+ background-color: ${({ hasText }) => (hasText ? "#e1e0ff" : "#e9eaee")};
+`;
+
+const Hash = styled.div`
+ ${tw`mr-2 text-[22px] font-semibold`}
+ color: ${({ hasText }) => (hasText ? "#5453ee" : "#676f7b")};
+`;
+
+const HashtagCount = styled.div`
+ ${tw`text-left text-[#b0b1b3] text-xs font-medium mt-2 ml-[10px]`}
+`;
+
+const ConfirmButton = styled.button`
+ ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`}
+`;
+
+const StyledInput = styled.input`
+ ${tw`w-full h-full rounded-lg text-main`}
+ background-color: transparent;
+ border: none;
+ outline: none;
+`;
\ No newline at end of file
diff --git a/src/components/PhotoCheck/RecordModal.tsx b/src/components/PhotoCheck/RecordModal.tsx
new file mode 100644
index 0000000..3392359
--- /dev/null
+++ b/src/components/PhotoCheck/RecordModal.tsx
@@ -0,0 +1,75 @@
+import styled from "styled-components";
+import tw from "twin.macro";
+import X from "../../assets/images/X.svg?react";
+import { useState } from "react";
+
+type ModalProps = {
+ closeModal: () => void;
+ setRecords: (tags: string) => void;
+}
+
+function RecordModal({ closeModal, setRecords }: ModalProps) {
+ const [words, setWords] = useState("");
+ const confirm = () => {
+ setRecords(words);
+ closeModal();
+ }
+
+ return (
+
+
+ 기록 추가
+ 자유롭게 사진에 대한 기록을 추가해보세요!
+
+
+
+
+
+ setWords(e.target.value)}
+ />
+
+
+ 확인
+
+
+)
+ ;
+}
+
+export default RecordModal;
+
+const ModalOverlay = styled.div`
+ ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`}
+ background-color: rgba(23, 28, 36, 0.8);
+`;
+
+const ModalContent = styled.div`
+ ${tw`w-[390px] h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center`}
+`;
+
+const Title = styled.h2`
+ ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`}
+`;
+
+const SubText = styled.p`
+ ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`}
+`;
+
+const CloseButton = styled.button`
+ ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`}
+`;
+
+const RecordContainer = styled.div`
+ ${tw`space-y-4 my-4 flex flex-col items-start`}
+`;
+
+const ConfirmButton = styled.button`
+ ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`}
+`;
+
+const StyledInput = styled.input`
+ ${tw`w-full h-full rounded-lg text-gray400 bg-gray100 flex-grow`}
+`;
\ No newline at end of file
diff --git a/src/components/WriteReview/InputTagSection.tsx b/src/components/WriteReview/InputTagSection.tsx
new file mode 100644
index 0000000..d069c70
--- /dev/null
+++ b/src/components/WriteReview/InputTagSection.tsx
@@ -0,0 +1,48 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import { Feature } from "../../@types/review";
+
+type InputTagProps = {
+ title: string;
+ features: Feature[];
+ selectedTags: number[];
+ setSelectedTags: React.Dispatch>;
+};
+function InputTagSection({ title, features, selectedTags, setSelectedTags }: InputTagProps) {
+ //클릭된 태그를 전달받아, 이미 선택된 태그라면 제거, 아니라면 추가하는 로직
+ const handleClick = (target: number) => {
+ if (selectedTags!.includes(target)) {
+ //이미 선택된 태그면 배열에서 제거
+ setSelectedTags(selectedTags!.filter((label) => label !== target));
+ } else {
+ //선택되지 않은 태그면 배열에 추가
+ setSelectedTags([...selectedTags!, target]);
+ }
+ };
+ return (
+
+
+ {features.map((tag, index) => (
+ handleClick(tag.id)}>
+ {tag.featureName}
+
+ ))}
+
+ );
+}
+
+export default InputTagSection;
+
+const Container = styled.div`
+ ${tw`flex flex-col w-full gap-[12px]`}
+
+ label {
+ ${tw`font-semibold text-[16px] text-gray700`}
+ }
+`;
+
+const TagBtn = styled.button<{ $active: boolean }>`
+ ${tw`px-[15px] py-[6px] font-display font-normal text-[16px] rounded-[24px] border-[1px]`}
+
+ ${({ $active }) => ($active ? tw`bg-purple border-main text-main` : tw`text-gray400 border-gray400`)}
+`;
diff --git a/src/components/WriteReview/UploadImageSection.tsx b/src/components/WriteReview/UploadImageSection.tsx
new file mode 100644
index 0000000..85ca4ac
--- /dev/null
+++ b/src/components/WriteReview/UploadImageSection.tsx
@@ -0,0 +1,74 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import CameraSvg from "../../assets/images/camera.svg?react";
+import { MdDeleteOutline } from "react-icons/md";
+type UploadImageProps = {
+ imageFiles: File[];
+ setImageFiles: React.Dispatch>;
+};
+function UploadImageSection({ imageFiles, setImageFiles }: UploadImageProps) {
+ const handleImageUpload = (event: React.ChangeEvent) => {
+ if (event.target.files) {
+ const filesArray = Array.from(event.target.files);
+ setImageFiles((prevFiles) => {
+ const newFiles = [...prevFiles, ...filesArray];
+ return newFiles;
+ });
+ }
+ };
+ const handleImageDelete = (index: number) => {
+ // File 객체 삭제
+ const updatedFiles = imageFiles.filter((_, i) => i !== index);
+ setImageFiles(updatedFiles);
+ };
+
+ return (
+
+
+
+
+ {imageFiles.map((file, index) => (
+
+
+ handleImageDelete(index)}>
+
+
+
+ ))}
+
+ );
+}
+
+export default UploadImageSection;
+
+const Container = styled.div`
+ ${tw`flex my-[25px] gap-[12px] overflow-x-auto`}
+`;
+
+const UploadButtonWrapper = styled.div`
+ ${tw`flex flex-col items-center justify-center w-[92px] h-[92px] bg-gray100 rounded-[8px]`}
+ span {
+ ${tw`font-display font-medium text-[12px] text-gray400 mt-[4px]`}
+ }
+`;
+
+const ImageWrapper = styled.div`
+ ${tw`relative w-[92px] h-[92px] rounded-[8px] flex-shrink-0`}
+`;
+
+const DeleteIconWrapper = styled.div`
+ ${tw`absolute top-1/2 left-[36px] cursor-pointer z-10`}
+`;
diff --git a/src/data/booth-categories.ts b/src/data/booth-categories.ts
new file mode 100644
index 0000000..ba8a739
--- /dev/null
+++ b/src/data/booth-categories.ts
@@ -0,0 +1,28 @@
+import HaruUrl from "../assets/images/haru-logo.png";
+import InsaengUrl from "../assets/images/insaeng-logo.png";
+import PhotoIsmUrl from "../assets/images/photoism-logo.png";
+import PhotoMaticUrl from "../assets/images/photomatic-logo.jpg";
+import SignatureUrl from "../assets/images/signature-logo.jpg";
+import LookUpUrl from "../assets/images/lookup-logo.png";
+import OldMoonUrl from "../assets/images/oldmoon-logo.jpg";
+import MonoUrl from "../assets/images/mono-logo.png";
+import GrayUrl from "../assets/images/gray-logo.jpeg";
+import PlanBUrl from "../assets/images/planb-logo.png";
+export type Category = {
+ id?: string;
+ label: string;
+ imageUrl: string;
+};
+
+export const BoothCategories: Category[] = [
+ { id: "LIFE4CUT", label: "인생네컷", imageUrl: InsaengUrl },
+ { id: "HARUFLIM", label: "하루필름", imageUrl: HaruUrl },
+ { id: "PHOTOISM", label: "포토이즘", imageUrl: PhotoIsmUrl },
+ { id: "PHOTOMATIC", label: "포토매틱", imageUrl: PhotoMaticUrl },
+ { id: "PHOTOSIGNATURE", label: "포토시그니처", imageUrl: SignatureUrl },
+ { id: "DONTLXXKUP", label: "돈룩업", imageUrl: LookUpUrl },
+ { id: "OLDMOON", label: "그믐달", imageUrl: OldMoonUrl },
+ { id: "MONOMANSION", label: "모노맨션", imageUrl: MonoUrl },
+ { id: "PHOTOGRAY", label: "포토그레이", imageUrl: GrayUrl },
+ { id: "PLANBSTUDIO", label: "플랜비스튜디오", imageUrl: PlanBUrl },
+];
diff --git a/src/data/review-tag-categories.ts b/src/data/review-tag-categories.ts
new file mode 100644
index 0000000..8b0e82e
--- /dev/null
+++ b/src/data/review-tag-categories.ts
@@ -0,0 +1,81 @@
+import { Category } from "./booth-categories";
+import CleanTool from "../assets/images/review_tags/tool.svg?url";
+import Sunglass from "../assets/images/review_tags/odd.svg?url";
+import Flower from "../assets/images/review_tags/selfie.svg?url";
+import MultipleBg from "../assets/images/review_tags/multiple_bg.svg?url";
+import LargeBooth from "../assets/images/review_tags/large_booth.svg?url";
+import LargeWait from "../assets/images/review_tags/large_wait.svg?url";
+import CleanBooth from "../assets/images/review_tags/clean_booth.svg?url";
+import MultipleFrame from "../assets/images/review_tags/multiple_frame.svg?url";
+import Powder from "../assets/images/review_tags/powder.svg?url";
+import CleanPhoto from "../assets/images/review_tags/clean-photo.svg?url";
+import Dark from "../assets/images/review_tags/dark-photo.svg?url";
+import Natural from "../assets/images/review_tags/natural.svg?url";
+import LightPhoto from "../assets/images/review_tags/light-photo.svg?url";
+import NoLight from "../assets/images/review_tags/no-light.svg?url";
+export const BoothTagCategories: Category[] = [
+ {
+ label: "깔끔한 소품",
+ imageUrl: CleanTool,
+ },
+ {
+ label: "홀수 출력 가능",
+ imageUrl: Sunglass,
+ },
+ {
+ label: "예쁜 셀카존",
+ imageUrl: Flower,
+ },
+ {
+ label: "다양한 배경색",
+ imageUrl: MultipleBg,
+ },
+ {
+ label: "넓은 대기 공간",
+ imageUrl: LargeWait,
+ },
+ {
+ label: "넓은 부스 공간",
+ imageUrl: LargeBooth,
+ },
+ {
+ label: "청결한 부스",
+ imageUrl: CleanBooth,
+ },
+ {
+ label: "다양한 프레임",
+ imageUrl: MultipleFrame,
+ },
+ {
+ label: "좋은 파우더룸",
+ imageUrl: Powder,
+ },
+];
+
+export const PhotoTagCategories: Category[] = [
+ {
+ label: "선명한 화질",
+ imageUrl: CleanPhoto,
+ },
+ {
+ label: "생각보다 어두움",
+ imageUrl: Dark,
+ },
+ {
+ label: "자연스러운 보정",
+ imageUrl: Natural,
+ },
+
+ {
+ label: "생각보다 밝음",
+ imageUrl: LightPhoto,
+ },
+ {
+ label: "빛번짐 없음",
+ imageUrl: NoLight,
+ },
+ {
+ label: "쿨톤 필터 가능",
+ imageUrl: CleanPhoto,
+ },
+];
diff --git a/src/hooks/getImageUrl.tsx b/src/hooks/getImageUrl.tsx
new file mode 100644
index 0000000..c950d4a
--- /dev/null
+++ b/src/hooks/getImageUrl.tsx
@@ -0,0 +1,22 @@
+import { BoothCategories } from "../data/booth-categories";
+import { BoothTagCategories, PhotoTagCategories } from "../data/review-tag-categories";
+
+// BoothLogoUrl에서 type에 맞는 url 찾기
+export const getLogoUrl = (type: string) => {
+ const logo = BoothCategories.find((item) => item.id === type);
+ console.log(type);
+ console.log(logo);
+ return logo!.imageUrl; // 해당 type에 맞는 로고 URL 반환
+};
+
+export const getReviewBoothTagImgUrl = (name: string) => {
+ const tag = BoothTagCategories.find((item) => item.label === name);
+ console.log(tag);
+ return tag!.imageUrl; // 해당 type에 맞는 로고 URL 반환
+};
+
+export const getReviewPhotoTagImgUrl = (name: string) => {
+ const tag = PhotoTagCategories.find((item) => item.label === name);
+ console.log(tag);
+ return tag!.imageUrl; // 해당 type에 맞는 로고 URL 반환
+};
diff --git a/src/hooks/getLocation.tsx b/src/hooks/getLocation.tsx
new file mode 100644
index 0000000..ec67806
--- /dev/null
+++ b/src/hooks/getLocation.tsx
@@ -0,0 +1,88 @@
+// const getIp = async () =>
+// await fetch("https://geolocation-db.com/json/")
+// .then((res) => res.json())
+// .then((res) => res["IPv4"]);
+
+// export const getCurrentLocation = async () => {
+// const nowIp = await getIp();
+// const geoData = await fetch(`http://ip-api.com/json/${nowIp}`)
+// .then((res) => res.json())
+// .then((res) => {
+// console.log(res);
+// return res;
+// });
+
+// if (geoData) {
+// const latitude = geoData.lat;
+// const longitude = geoData.lon;
+
+// return { lat: latitude, lng: longitude };
+// }
+// };
+
+export const getCurrentLocation = async () => {
+ if (navigator.geolocation) {
+ return new Promise<{ lat: number; lng: number }>((resolve, reject) => {
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ resolve({ lat, lng });
+ },
+ (error) => {
+ reject(error);
+ }
+ );
+ });
+ }
+ // 사용자의 위치 정보를 가져올 수 없는 경우, 기본 위치 리턴
+ return { lat: 37.63763525003301, lng: 127.07945581420265 };
+};
+
+export const trackCurrentPosition = (
+ onUpdate: (position: { lat: number; lng: number; heading: number | null }) => void
+) => {
+ if (navigator.geolocation) {
+ const watchId = navigator.geolocation.watchPosition(
+ (position) => {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ const heading = position.coords.heading; // 장치의 방향 정보
+ console.log(lat, lng, heading);
+ // 새로운 위치와 방향을 업데이트하는 콜백 호출
+ onUpdate({ lat, lng, heading });
+ },
+ (error) => {
+ console.error("Geolocation error:", error);
+ },
+ { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
+ );
+
+ // 추적을 멈추고 싶을 때 호출할 수 있는 clear 함수 반환
+ return () => navigator.geolocation.clearWatch(watchId);
+ }
+
+ console.error("Geolocation is not supported by this browser.");
+ return () => {};
+};
+
+export const getDistance = (lat1: number, lng1: number, lat2: number, lng2: number): string => {
+ const R = 6371; // Radius of the Earth in kilometers
+ const dLat = (lat2 - lat1) * (Math.PI / 180); // Convert latitude difference to radians
+ const dLon = (lng2 - lng1) * (Math.PI / 180); // Convert longitude difference to radians
+
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
+
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ const distanceInKm = R * c; // Distance in kilometers
+
+ // Convert distance to meters if less than 1 km, otherwise keep it in kilometers
+ if (distanceInKm < 1) {
+ const distanceInMeters = distanceInKm * 1000; // Convert km to meters
+ return `${distanceInMeters.toFixed(0)} m`;
+ } else {
+ return `${distanceInKm.toFixed(2)} km`;
+ }
+};
diff --git a/src/index.css b/src/index.css
index 06597c6..b60926a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
@@ -24,3 +25,36 @@ body {
margin: auto; /* 가운데 정렬 */
background-color: #f2f2f2;
}
+::-webkit-scrollbar {
+ display: none;
+}
+.dots_custom {
+ display: inline-block;
+ vertical-align: middle;
+ margin: auto 0;
+ padding: 0;
+}
+
+.dots_custom li {
+ list-style: none;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0 6px;
+ padding: 0;
+}
+
+.dots_custom li button {
+ border: none;
+ background: #ffffff;
+ color: transparent;
+ cursor: pointer;
+ display: block;
+ height: 8px;
+ width: 8px;
+ border-radius: 100%;
+ padding: 0;
+}
+
+.dots_custom li.slick-active button {
+ background-color: #5453ee;
+}
diff --git a/src/main.tsx b/src/main.tsx
index 0ebf5a7..d452219 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,9 +2,20 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+const queryClient = new QueryClient();
+// const ReactQueryDevtoolsProduction = React.lazy(() =>
+// import("@tanstack/react-query-devtools/build/lib/index.prod.js").then((d) => ({
+// default: d.ReactQueryDevtools,
+// }))
+// );
createRoot(document.getElementById("root")!).render(
-
+
+
+
+
);
diff --git a/src/pages/Album/index.tsx b/src/pages/Album/index.tsx
index 4c4657f..5168cc7 100644
--- a/src/pages/Album/index.tsx
+++ b/src/pages/Album/index.tsx
@@ -1,12 +1,252 @@
-import React from "react";
+import { useState, useEffect } from "react";
import NavBar from "../../components/Common/NavBar";
+import Search from "../../assets/images/search.svg?react";
+import NoImage from "../../assets/images/no-images.svg?react";
+import styled from "styled-components";
+import tw from "twin.macro";
+import ImageCard from "../../components/Album/ImageCard.tsx";
+import DateModal from "../../components/Album/DateModal.tsx";
+import Footer from "../../components/Album/Footer.tsx";
+
+type Images = {
+ url: string;
+ title: string;
+}
function Album() {
+ const [selectedCategory, setSelectedCategory] = useState("날짜별");
+ const [isDateModalOpen, setIsDateModalOpen] = useState(false);
+ const [imageList, setImageList] = useState([]);
+ const [isEditing, setIsEditing] = useState(false);
+ const [date, setDate] = useState("2024년 9월");
+
+ useEffect(() => {
+ setImageList([
+ {
+ url: "https://example.com/image1.jpg",
+ title: "Sample Image 1",
+ },
+ {
+ url: "https://example.com/image2.jpg",
+ title: "Sample Image 2",
+ },
+ {
+ url: "https://example.com/image3.jpg",
+ title: "Sample Image 3",
+ },
+ {
+ url: "https://example.com/image4.jpg",
+ title: "Sample Image 4",
+ },
+ {
+ url: "https://example.com/image5.jpg",
+ title: "Sample Image 5",
+ },
+ {
+ url: "https://example.com/image6.jpg",
+ title: "Sample Image 6",
+ },
+ {
+ url: "https://example.com/image7.jpg",
+ title: "Sample Image 7",
+ },
+ {
+ url: "https://example.com/image8.jpg",
+ title: "Sample Image 8",
+ },
+ ]);
+ }, []);
+
+ // "선택" 버튼 클릭 핸들러
+ const handleSelectClick = () => {
+ setIsEditing(true);
+ };
+
+ const handleCancelClick = () => {
+ setIsEditing(false);
+ };
+
+ const handleAddClick = () => {
+
+ }
+
+ // 카테고리 버튼 클릭 핸들러
+ const handleCategoryClick = (category: string) => {
+ setSelectedCategory(category);
+ };
+
+ const handleCloseModal = () => {
+ setIsDateModalOpen(false);
+ }
return (
-
-
-
+
+
+
+
+
+
+
+ setIsDateModalOpen(true)}>{date}
+
+
+
+
+ {isEditing ? (
+
취소
+ ) : (
+ <>
+
선택
+
추가
+ >
+ )}
+
+
+
+ {}
+ {isDateModalOpen && }
+
+ {imageList.length === 0 ? (
+ <>
+
+ 사진을 채워보세요
+ >
+ ) : (
+
+ {imageList.map((image,index) =>(
+
+ ))}
+
+ )}
+
+
+ {!isEditing && (
+
+ handleCategoryClick("날짜별")}>
+ 날짜별
+
+ handleCategoryClick("포토부스별")}
+ >
+ 포토부스별
+
+ handleCategoryClick("위치별")}>
+ 위치별
+
+
+ )}
+
+ {isEditing ? (
+
+ ) : (
+
+ )}
+
);
}
+// Styled Components
+
+const Layout = styled.div`
+ ${tw`flex flex-col w-full h-screen items-center bg-[#FFFFFF]`}
+ max-width: 480px;
+`;
+
+const ImageContainer = styled.div`
+ ${tw`flex flex-col justify-center items-center w-full`}
+ height: calc(100vh - 220px); /* 높이를 조정하여 다른 UI 요소가 가리지 않도록 */
+ overflow-y: auto;
+ position: relative;
+ z-index: 1; /* 다른 요소들보다 낮게 설정 */
+`;
+
+const ImageDiv = styled.div`
+ ${tw`grid gap-4 w-full`}
+ grid-template-columns: repeat(2, 1fr); /* 두 개의 열 */
+ grid-auto-rows: auto;
+ max-height: 100%; /* 부모 컨테이너 안에서 최대 높이 제한 */
+ overflow-y: auto;
+`;
+
+const Content = styled.div`
+ ${tw`flex flex-col w-full px-4 py-6 gap-4`}
+ height: calc(100vh - 60px);
+`;
+
+const HeaderSection = styled.div`
+ ${tw`flex flex-col items-center`}
+ position: relative;
+ z-index: 10;
+`;
+
+const Subtitle = styled.div`
+ ${tw`h-[33px] px-4 py-1.5 bg-[#5453ee] justify-center items-center inline-flex rounded-full shadow text-background text-base font-semibold`}
+ font-family: 'Pretendard', sans-serif;
+`;
+
+const ButtonGroup = styled.div`
+ ${tw`flex gap-2 justify-between relative`}
+ position: relative;
+ z-index: 10;
+`;
+
+const PositionedDiv = styled.div`
+ ${tw`absolute mt-2`}
+ top: 100%;
+ left: 50%;
+ transform: translateX(-75%);
+ margin-top: 1px;
+`;
+
+const ActionButton = styled.div`
+ ${tw`h-[33px] px-4 py-1.5 rounded-full text-[#4b515a] bg-[#c7c9ce] shadow text-center text-sm font-semibold cursor-pointer`}
+ font-family: 'Pretendard', sans-serif;
+`;
+
+const CategoryMenu = styled.div`
+ ${tw`flex bg-[#c7c9ce]/80 rounded-full shadow`}
+ width: 248.1px;
+ position: fixed;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-bottom: 70px;
+ z-index: 10;
+`;
+
+const CategoryItem = styled.div<{ selected?: boolean }>`
+ ${tw`text-base font-semibold cursor-pointer flex items-center justify-center`}
+ height: 38px;
+ padding: 0 16px;
+ border-radius: 30px;
+ color: ${({ selected }) => (selected ? '#fff' : '#676f7b')};
+ background-color: ${({ selected }) => (selected ? '#676f7b' : 'transparent')};
+ font-family: 'Pretendard', sans-serif;
+ white-space: nowrap;
+ transition: background-color 0.3s ease, color 0.3s ease;
+`;
+
export default Album;
diff --git a/src/pages/Booth/FeedPage.tsx b/src/pages/Booth/FeedPage.tsx
new file mode 100644
index 0000000..6a3662d
--- /dev/null
+++ b/src/pages/Booth/FeedPage.tsx
@@ -0,0 +1,48 @@
+import BoothRating from "../../components/Booth/BoothRating";
+import FeedImgList from "../../components/Booth/FeedImgList";
+import FeedTagSection from "../../components/Booth/FeedTagSection";
+import ReviewListSection from "../../components/Booth/ReviewListSection";
+import { useParams } from "react-router-dom";
+import { useAuthStore } from "../../store/useAuthStore";
+import { useQuery } from "@tanstack/react-query";
+import { getBoothTags, getPhotoTags, getRecentReviews } from "../../api/review";
+function FeedPage() {
+ const { boothId } = useParams() as { boothId: string };
+
+ const { accessToken } = useAuthStore();
+ //최근 리뷰 조회
+ const { isLoading, data } = useQuery({
+ queryKey: ["getRecentReviews", boothId],
+ queryFn: () => getRecentReviews(boothId, accessToken!),
+ });
+
+ const { data: BoothTags } = useQuery({
+ queryKey: ["getBoothTags", boothId],
+ queryFn: () => getBoothTags(boothId, accessToken!),
+ });
+
+ const { data: PhotoTags } = useQuery({
+ queryKey: ["getPhotoTags", boothId],
+ queryFn: () => getPhotoTags(boothId, accessToken!),
+ });
+
+ return (
+
+
+
+
+
+ {BoothTags && BoothTags.length > 0 && (
+
+ )}
+
+ {PhotoTags && PhotoTags.length > 0 && (
+
+ )}
+
+ {!isLoading && data &&
}
+
+ );
+}
+
+export default FeedPage;
diff --git a/src/pages/Booth/ImagePage.tsx b/src/pages/Booth/ImagePage.tsx
new file mode 100644
index 0000000..16c74ed
--- /dev/null
+++ b/src/pages/Booth/ImagePage.tsx
@@ -0,0 +1,22 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+function ImagePage() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default ImagePage;
+
+const Container = styled.div`
+ ${tw`w-full p-[16px] grid gap-[8px] `}
+ grid-template-columns: repeat(2, 1fr);
+
+ .img-container {
+ ${tw`w-full [aspect-ratio: 1 / 1] rounded-[8px] bg-gray200`}
+ }
+`;
diff --git a/src/pages/Booth/ReviewPage.tsx b/src/pages/Booth/ReviewPage.tsx
new file mode 100644
index 0000000..18cf494
--- /dev/null
+++ b/src/pages/Booth/ReviewPage.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import BoothDetail from ".";
+import FeedTagSection from "../../components/Booth/FeedTagSection";
+import ReviewListSection from "../../components/Booth/ReviewListSection";
+import { useQuery } from "@tanstack/react-query";
+import { getBoothTags, getPhotoTags, getRecentReviews } from "../../api/review";
+import { useParams } from "react-router-dom";
+import { useAuthStore } from "../../store/useAuthStore";
+
+function ReviewPage() {
+ const { boothId } = useParams() as { boothId: string };
+ const { accessToken } = useAuthStore();
+
+ const { isLoading, data } = useQuery({
+ queryKey: ["getRecentReviews", boothId],
+ queryFn: () => getRecentReviews(boothId, accessToken!),
+ });
+
+ const { isLoading: boothTagLoading, data: BoothTags } = useQuery({
+ queryKey: ["getBoothTags", boothId],
+ queryFn: () => getBoothTags(boothId, accessToken!),
+ });
+
+ const { isLoading: photoTagLoading, data: PhotoTags } = useQuery({
+ queryKey: ["getPhotoTags", boothId],
+ queryFn: () => getPhotoTags(boothId, accessToken!),
+ });
+ return (
+
+ {!boothTagLoading && BoothTags && BoothTags.length > 0 && (
+
+ )}
+
+ {!photoTagLoading && PhotoTags && PhotoTags.length > 0 && (
+
+ )}
+
+ {!isLoading && data &&
}
+
+ );
+}
+
+export default ReviewPage;
diff --git a/src/pages/Booth/index.tsx b/src/pages/Booth/index.tsx
new file mode 100644
index 0000000..335a172
--- /dev/null
+++ b/src/pages/Booth/index.tsx
@@ -0,0 +1,71 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import NavBar from "../../components/Common/NavBar";
+import Header from "../../components/Common/Header";
+import ImgSlider from "../../components/Booth/ImgSlider";
+import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
+import BoothInfoSection from "../../components/Booth/BoothInfo";
+import { useQuery } from "@tanstack/react-query";
+import { getBoothInfo } from "../../api/booth";
+import { useAuthStore } from "../../store/useAuthStore";
+
+function BoothDetail() {
+ const locationNow = useLocation();
+ const { boothId } = useParams() as { boothId: string };
+ const { accessToken } = useAuthStore();
+ //특정 포토부스 정보 조회 api 호출
+ const { isLoading, data: boothInfo } = useQuery({
+ queryKey: ["getBoothInfo", boothId],
+ queryFn: () => getBoothInfo(boothId, accessToken!),
+ });
+
+ const navigate = useNavigate();
+ return (
+
+ {!isLoading && boothInfo && navigate("/home")} />}
+
+ {/* */}
+ {!isLoading && boothInfo && (
+
+ )}
+
+ navigate("feed")}>
+ 홈
+
+ navigate("review")}>
+ 리뷰
+
+ navigate("image")}>
+ 사진
+
+
+ {/* 하위 홈, 리뷰, 사진 영역 컴포넌트 렌더링 */}
+
+
+
+
+ );
+}
+
+export default BoothDetail;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`}
+`;
+
+const MainWrapper = styled.div`
+ ${tw`overflow-auto flex flex-col w-full mt-[60px] items-center pb-[80px]`}
+`;
+
+const MenuContainer = styled.div`
+ ${tw`flex flex-row w-full h-[32px] border-b-2 border-b-gray100 mt-[30px] justify-between px-[30px]`}
+`;
+const MenuBtn = styled.button<{ $active: boolean }>`
+ ${tw`w-[67px] h-[31px] font-display font-semibold text-[14px]`}
+ ${({ $active }) => ($active ? tw`text-gray700 border-b-4 border-b-main ` : tw`text-gray300`)}
+`;
diff --git a/src/pages/CompleteScreen.tsx b/src/pages/CompleteScreen.tsx
new file mode 100644
index 0000000..e2b0075
--- /dev/null
+++ b/src/pages/CompleteScreen.tsx
@@ -0,0 +1,32 @@
+import { useEffect, useState } from "react";
+import tw from "twin.macro";
+import styled from "styled-components";
+import CompleteSvg from "../assets/images/complete-review.svg?react";
+import { useNavigate } from "react-router-dom";
+function CompleteScreen() {
+ const [opacity, setOpacity] = useState(100);
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (opacity > 0) {
+ const timer = setTimeout(() => {
+ setOpacity((prev) => Math.max(prev - 8, 0)); // 상태 업데이트
+ }, 200);
+
+ return () => clearTimeout(timer); // 타이머 정리
+ } else {
+ // 페이지 이동
+ navigate("/home");
+ }
+ }, [opacity]);
+ return (
+
+
+ 소중한 의견이 등록되었어요!
+
+ );
+}
+
+export default CompleteScreen;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center justify-center font-display bg-[#FFFFFF]`}
+`;
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
index 4f169aa..6726ce0 100644
--- a/src/pages/Home/index.tsx
+++ b/src/pages/Home/index.tsx
@@ -1,15 +1,26 @@
-// import tw from "twin.macro";
-// import styled from "styled-components";
+import tw from "twin.macro";
+import styled from "styled-components";
import NavBar from "../../components/Common/NavBar";
+import Search from "../../components/Home/Search";
+import AddBtn from "../../components/Home/AddBtn";
+import BoothSlide from "../../components/Home/BoothSlide";
+import BoothMap from "../../components/Home/BoothMap";
function Home() {
- console.log("Home component rendered"); // 디버깅용 로그
-
return (
-
+
);
}
export default Home;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto`}
+`;
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..8dd3bed
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,33 @@
+///
+import LoginBanner from "../assets/images/login-banner.svg?react";
+import KaKaoImg from "../assets/images/kakao.svg?react";
+import tw from "twin.macro";
+import styled from "styled-components";
+function LoginPage() {
+ const handleLogin = () => {
+ window.location.href = "https://pocket4cut.link/oauth2/authorization/kakao";
+ };
+ return (
+
+
+
+
+ 카카오톡으로 계속하기
+
+
+ );
+}
+
+export default LoginPage;
+
+const Container = styled.div`
+ ${tw`flex flex-col items-center justify-center w-full h-[100vh] bg-[#FFFFFF]`}
+`;
+
+const LoginBtn = styled.button`
+ ${tw`w-[310px] h-[56px] flex flex-row items-center bg-[#FBE300] rounded-[8px] justify-center mt-[30px] `}
+
+ span {
+ ${tw`font-display font-semibold text-16 text-[#000000] cursor-pointer ml-3.5`}
+ }
+`;
diff --git a/src/pages/My/FavoritesPage.tsx b/src/pages/My/FavoritesPage.tsx
new file mode 100644
index 0000000..0b931ae
--- /dev/null
+++ b/src/pages/My/FavoritesPage.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import tw from "twin.macro";
+import styled from "styled-components";
+import ImageCard from "../../components/Album/ImageCard";
+function FavoritesPage() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default FavoritesPage;
+const Container = styled.div`
+ ${tw`w-full grid gap-[10px] mt-[10px] px-[16px]`}
+ grid-template-columns: repeat(2, 1fr);
+`;
diff --git a/src/pages/My/LikeBoothsPage.tsx b/src/pages/My/LikeBoothsPage.tsx
new file mode 100644
index 0000000..c05776c
--- /dev/null
+++ b/src/pages/My/LikeBoothsPage.tsx
@@ -0,0 +1,25 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import Header from "../../components/Common/Header";
+import { useNavigate } from "react-router-dom";
+import LikeBoothCard from "../../components/My/LikeBoothCard";
+function LikeBoothsPage() {
+ const navigate = useNavigate();
+ return (
+
+
+ );
+}
+
+export default LikeBoothsPage;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`}
+`;
+
+const CardContainer = styled.div`
+ ${tw`w-full flex flex-col px-[15px] mt-[85px] gap-[14px]`}
+`;
diff --git a/src/pages/My/MyReviewPage.tsx b/src/pages/My/MyReviewPage.tsx
new file mode 100644
index 0000000..bd6e4fe
--- /dev/null
+++ b/src/pages/My/MyReviewPage.tsx
@@ -0,0 +1,26 @@
+import Header from "../../components/Common/Header";
+import { useNavigate } from "react-router-dom";
+import tw from "twin.macro";
+import styled from "styled-components";
+import MyReviewCard from "../../components/My/MyReviewCard";
+function MyReviewPage() {
+ const navigate = useNavigate();
+ return (
+
+ navigate(-1)} />
+
+
+
+
+
+ );
+}
+
+export default MyReviewPage;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`}
+`;
+const ImgBox = styled.div`
+ ${tw`w-full grid gap-[10px] mt-[60px] p-[15px]`}
+ grid-template-columns: repeat(2, 1fr);
+`;
diff --git a/src/pages/My/RecordPage.tsx b/src/pages/My/RecordPage.tsx
new file mode 100644
index 0000000..b991a93
--- /dev/null
+++ b/src/pages/My/RecordPage.tsx
@@ -0,0 +1,85 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import RightArrowIcon from "../../assets/icons/right-arrow";
+import MyReviewCard from "../../components/My/MyReviewCard";
+import { useNavigate } from "react-router-dom";
+import LikeBoothCard from "../../components/My/LikeBoothCard";
+import VisitedBoothCard from "../../components/My/VisitedBoothCard";
+function RecordPage() {
+ const navigate = useNavigate();
+ return (
+
+
+ 24개의 나의 리뷰
+
+
+
+
+
+
+
+
+
+ 찜해둔 부스
+
+
+
+
+
+
+
+
+ 방문한 부스
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RecordPage;
+
+const Container = styled.div`
+ ${tw`w-full flex flex-col font-display my-[15px]`}
+ .title {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+ .more-btn {
+ ${tw`w-[66px] h-[22px] rounded-[24px] bg-gray100 font-normal text-[12px] text-gray400 flex gap-1 items-center justify-center`}
+ }
+`;
+
+const ImgBox = styled.div`
+ ${tw`w-full grid gap-[10px] mt-[10px] px-[16px]`}
+ grid-template-columns: repeat(2, 1fr);
+`;
+
+const SlideWrapper = styled.div`
+ ${tw`flex gap-2 overflow-x-auto mt-[10px] ml-[16px]`}
+ scroll-snap-type: x mandatory; /* 각 버튼이 스냅되게 설정 */
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ -ms-overflow-style: none; /* IE and 엣지 */
+ scrollbar-width: none; /* 파이어폭스 */
+ -webkit-overflow-scrolling: touch; /* 모바일 환경에서 터치 스크롤 부드럽게 처리 */
+
+ /* 자식 카드 크기를 유지하기 위해 min-width 적용 */
+ > div {
+ min-width: 292px; /* 카드의 너비에 맞춰 설정 */
+ scroll-snap-align: start; /* 스냅을 카드 시작 지점에 맞춤 */
+ }
+`;
diff --git a/src/pages/My/VisitedBoothsPage.tsx b/src/pages/My/VisitedBoothsPage.tsx
new file mode 100644
index 0000000..9c9407d
--- /dev/null
+++ b/src/pages/My/VisitedBoothsPage.tsx
@@ -0,0 +1,26 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import Header from "../../components/Common/Header";
+import VisitedBoothCard from "../../components/My/VisitedBoothCard";
+import { useNavigate } from "react-router-dom";
+function VisitedBoothsPage() {
+ const navigate = useNavigate();
+ return (
+
+
+ );
+}
+
+export default VisitedBoothsPage;
+
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`}
+`;
+
+const CardContainer = styled.div`
+ ${tw`w-full flex flex-col px-[15px] mt-[85px] gap-[14px]`}
+`;
diff --git a/src/pages/My/index.tsx b/src/pages/My/index.tsx
index 713f1ee..9731b65 100644
--- a/src/pages/My/index.tsx
+++ b/src/pages/My/index.tsx
@@ -1,12 +1,43 @@
-import React from "react";
import NavBar from "../../components/Common/NavBar";
-
+import tw from "twin.macro";
+import styled from "styled-components";
+import ProfileSection from "../../components/My/ProfileSection";
+import { Outlet, useLocation, useNavigate } from "react-router-dom";
function My() {
+ const locationNow = useLocation();
+ const navigate = useNavigate();
return (
-
+
+
+
+
+ navigate("/my/booth-records")}
+ >
+ 부스기록
+
+ navigate("/my/favorites")}>
+ 즐겨찾기
+
+
+ {/* 하위 홈, 리뷰, 사진 영역 컴포넌트 렌더링 */}
+
+
-
+
);
}
export default My;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`}
+`;
+
+const MenuContainer = styled.div`
+ ${tw`flex flex-row w-full justify-center h-[32px] border-b-2 border-b-gray100 mt-[30px] px-[45px] gap-[100px]`}
+`;
+const MenuBtn = styled.button<{ $active: boolean }>`
+ ${tw`w-[105px] h-[31px] font-display font-semibold text-[14px]`}
+ ${({ $active }) => ($active ? tw`text-gray700 border-b-4 border-b-main ` : tw`text-gray700`)}
+`;
diff --git a/src/pages/PhotoCheck/index.tsx b/src/pages/PhotoCheck/index.tsx
new file mode 100644
index 0000000..f6f38e1
--- /dev/null
+++ b/src/pages/PhotoCheck/index.tsx
@@ -0,0 +1,91 @@
+import { useState, useEffect } from "react";
+import ShareComplete from '../../assets/images/share-complete.png';
+import PhotoCheck1 from "./step1.tsx";
+import PhotoCheck2 from "./step2.tsx";
+import PhotoCheck3 from "./step3.tsx";
+import { useLocation } from "react-router-dom";
+import {useNavigate} from "react-router-dom";
+
+type InfoState = {
+ year: string;
+ month: string;
+ day: string;
+ boothLocation: string;
+ qrLink: string;
+};
+
+function PhotoCheck() {
+ const location = useLocation();
+ const { year, month, day, boothLocation, qrLink } = location.state as InfoState || {};
+ const dateInfo = year + "년 " + month + "월 " + day + "일 " + boothLocation;
+ const [step, setStep] = useState(1);
+ const [hashTags, setHashTags] = useState([]);
+ const [records, setRecords] = useState("클릭하여 오늘 있었던 일들을 기록해보세요");
+ const navigate = useNavigate();
+
+ console.log(qrLink);
+
+ // Function to handle the next button click
+ const handleNextClick = () => {
+ setStep((prevStep) => prevStep + 1); // Increment the step state
+ };
+
+ const handleBackStep = () => {
+ setStep((prevStep) => prevStep - 1);
+ }
+
+ useEffect(() => {
+ if (step === 4) {
+ const timeout = setTimeout(() => {
+ navigate('/home'); // 2초 후에 home으로 리디렉션
+ }, 2000);
+ return () => clearTimeout(timeout);
+ }
+ }, [step]);
+
+ return (
+ <>
+ {step === 1 && (
+
+ )}
+
+ {step === 2 && (
+
+ )}
+
+ {step === 3 && (
+
+ )}
+
+ {step === 4 && (
+
+
+
공유가 완료됐어요
+
+ )}
+ >
+ );
+}
+
+export default PhotoCheck;
diff --git a/src/pages/PhotoCheck/step1.tsx b/src/pages/PhotoCheck/step1.tsx
new file mode 100644
index 0000000..967295a
--- /dev/null
+++ b/src/pages/PhotoCheck/step1.tsx
@@ -0,0 +1,58 @@
+import { useNavigate } from "react-router-dom";
+import styled from "styled-components";
+import tw from "twin.macro";
+import BackIcon from "../../assets/icons/back-icon.tsx";
+
+type Step1Props = {
+ handleNextClick: () => void;
+ dateInfo: string;
+ qrLink : string;
+}
+
+function PhotoCheck1({ handleNextClick, dateInfo, qrLink }: Step1Props) {
+ const navigate = useNavigate();
+
+ const handleBack = () => {
+ navigate("/photo-review");
+ };
+
+ return (
+
+
+ 사진 확인
+ {dateInfo}
+
+
+
+
+
+ 다음
+
+
+ );
+}
+
+const Container = styled.div`
+ ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`}
+ overflow-x: hidden;
+`;
+
+const Header = styled.header`
+ ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`}
+`;
+
+const Title = styled.div`
+ ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`}
+`;
+
+const DateText = styled.div`
+ ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`}
+`;
+
+const ButtonContainer = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 mb-[72px] flex justify-center items-center`}
+`;
+
+export default PhotoCheck1;
diff --git a/src/pages/PhotoCheck/step2.tsx b/src/pages/PhotoCheck/step2.tsx
new file mode 100644
index 0000000..4d011e5
--- /dev/null
+++ b/src/pages/PhotoCheck/step2.tsx
@@ -0,0 +1,153 @@
+import styled from "styled-components";
+import tw from "twin.macro";
+import BackIcon from "../../assets/icons/back-icon.tsx";
+import React, { useEffect, useState } from "react";
+import HashTagModal from "../../components/PhotoCheck/HashModal.tsx";
+import RecordModal from "../../components/PhotoCheck/RecordModal.tsx";
+
+type Step2Props = {
+ handleNextClick: () => void;
+ handleBackStep: () => void;
+ hashTags: string[];
+ setHashTags: React.Dispatch>;
+ records: string;
+ setRecords: React.Dispatch>;
+ dateInfo: string;
+ qrLink: string;
+}
+
+function PhotoCheck2({ handleNextClick, handleBackStep, hashTags, setHashTags, records, setRecords, dateInfo, qrLink }: Step2Props) {
+ const [isHashModalOpen, setIsHashModalOpen] = useState(false);
+ const [isRecordModalOpen, setIsRecordModalOpen] = useState(false);
+ const [countHash, setCountHash] = useState(0);
+
+ const openHashModal = () => {
+ setIsHashModalOpen(true);
+ };
+
+ const closeHashModal = () => {
+ setIsHashModalOpen(false);
+ };
+
+ const openRecordModal = () => {
+ setIsRecordModalOpen(true);
+ };
+
+ const closeRecordModal = () => {
+ setIsRecordModalOpen(false);
+ };
+
+ useEffect(() => {
+ const count = hashTags.filter((tag) => tag.length > 0);
+ setCountHash(count.length);
+ }, [hashTags]);
+
+ return (
+
+
+
+
사진 기록
+
+
+ {dateInfo}
+
+
+ {countHash > 0 ? (
+
+ {hashTags.map(
+ (tag, index) =>
+ tag && (
+
setIsHashModalOpen(true)}>
+
+
+ )
+ )}
+
{countHash}/3
+
+ ) : (
+ <>
+
+
+
+
+
+ {countHash}/3
+
+ >
+ )}
+
+ {isHashModalOpen && }
+
+
+
+
+ {isRecordModalOpen && }
+
+ 다음
+
+
+ );
+}
+
+const Container = styled.div`
+ ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`}
+ overflow-x: hidden;
+`;
+
+const Header = styled.header`
+ ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`}
+`;
+
+const Title = styled.div`
+ ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`}
+`;
+
+const DateText = styled.div`
+ ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`}
+`;
+
+const ButtonContainer = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 flex justify-center items-center`}
+`;
+
+const TagItem = styled.div`
+ ${tw`mb-2`}
+`;
+
+export default PhotoCheck2;
diff --git a/src/pages/PhotoCheck/step3.tsx b/src/pages/PhotoCheck/step3.tsx
new file mode 100644
index 0000000..1c9584a
--- /dev/null
+++ b/src/pages/PhotoCheck/step3.tsx
@@ -0,0 +1,116 @@
+import { useNavigate } from "react-router-dom";
+import ShareLogo from "../../assets/images/share-logo.svg?react";
+import Checked from "../../assets/images/checked.svg?react";
+import styled from "styled-components";
+import tw from "twin.macro";
+import BackIcon from "../../assets/icons/back-icon.tsx";
+import { useState } from "react";
+import axios from "axios";
+
+type Step3Props = {
+ handleNextClick: () => void;
+ handleBackStep: () => void;
+ dateInfo: string;
+ records:string;
+ hashtags:string[];
+ year:string;
+ month:string;
+ day:string;
+}
+
+function PhotoCheck3({ handleNextClick, handleBackStep, year, month, day, hashtags, records, dateInfo }: Step3Props) {
+ const navigate = useNavigate();
+ const [clicked, setClicked] = useState(false);
+
+ const handleSavePicture = async () => {
+ try {
+ const response = await axios.post('http://pocket4cut.link/api/v1/album', {
+ photoboothId: 0,
+ year: {year},
+ month: {month},
+ date: {day},
+ hashtag: {hashtags},
+ memo: {records},
+ filePath: "string"
+ },{
+ headers : {
+ 'Content-Type' : 'application/json'
+ }
+ }
+ )
+ alert(response.data);
+ } catch (error : any) {
+ // 여기 추후에 간소화 필요 2024.10.30
+ if (error.response) {
+ console.log("Data:", error.response.data);
+ console.log("Status:", error.response.status);
+ console.log("Headers:", error.response.headers);
+ alert(`Server responded with status: ${error.response.status}`);
+ } else if (error.request) {
+ console.log("Request:", error.request);
+ alert("No response received from the server.");
+ } else {
+ console.log("Error:", error.message);
+ alert(`Error in setting up the request: ${error.message}`);
+ }
+ }
+ };
+
+ return (
+
+
+
+
사진 확인
+
+
+ {dateInfo}
+
+
+
+
+
해시태그, 사진 기록까지 공유하기
+
+ {
+ handleNextClick()
+ handleSavePicture()
+ }}>
+ 다음
+
+ navigate("/home")}>
+ 다음에 할게요
+
+
+ );
+}
+
+const Container = styled.div`
+ ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`}
+ overflow-x: hidden;
+`;
+
+const Header = styled.header`
+ ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`}
+`;
+
+const Title = styled.div`
+ ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`}
+`;
+
+const DateText = styled.div`
+ ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`}
+`;
+
+const ButtonContainer = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 flex justify-center items-center`}
+`;
+
+const ButtonContainer2 = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#F9F9FB] rounded-lg mt-3 flex justify-center items-center`}
+`;
+
+export default PhotoCheck3;
diff --git a/src/pages/PhotoReview/index.tsx b/src/pages/PhotoReview/index.tsx
new file mode 100644
index 0000000..f39e3ee
--- /dev/null
+++ b/src/pages/PhotoReview/index.tsx
@@ -0,0 +1,176 @@
+import styled from "styled-components";
+import tw from "twin.macro";
+import Search from "../../assets/images/search.svg?react";
+import BackIcon from "../../assets/icons/back-icon";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useState, useEffect } from "react";
+
+function PhotoReview() {
+ // useState를 사용하여 날짜와 부스 위치 상태 관리
+ const [year, setYear] = useState("");
+ const [month, setMonth] = useState("");
+ const [day, setDay] = useState("");
+ const [boothLocation, setBoothLocation] = useState("");
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const handleNext = () => {
+ navigate("/photo-check", {
+ state: {
+ year: year,
+ month: month,
+ day: day,
+ boothLocation: boothLocation,
+ qrLink : location.state.qrLink,
+ },
+ });
+ };
+
+ const handleBack = () => {
+ navigate("/qr-scan");
+ };
+
+ return (
+
+
+
+
+
+
+
+ 필수
+
+
+
+
+
+ setYear(e.target.value)}
+ maxLength={4}
+ />
+
+
+ 년
+
+
+
+
+ setMonth(e.target.value)}
+ maxLength={2}
+ />
+
+
+ 월
+
+
+
+
+ setDay(e.target.value)}
+ maxLength={2}
+ />
+
+
+ 일
+
+
+
+
+
+
+ 필수
+
+
+
+
+
+
+ setBoothLocation(e.target.value)}
+ />
+
+
+ handleNext()}>
+ 다음
+
+
+ );
+}
+
+const Container = styled.div`
+ ${tw`bg-background flex flex-col w-full min-h-screen items-center `}
+ overflow-x: hidden;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ -ms-overflow-style: none;
+`;
+
+// const Header = styled.header`
+// ${tw`w-full flex flex-col place-items-center p-6 relative`}
+// `;
+
+const Title = styled.div`
+ ${tw`text-[#171d24] text-2xl font-semibold font-['Pretendard']`}
+`;
+
+const DateText = styled.div`
+ ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`}
+`;
+
+const ContentContainer = styled.div`
+ ${tw`flex flex-col items-start w-11/12 mt-10 gap-6`}
+`;
+
+const LabelContainer = styled.div`
+ ${tw`flex items-center gap-2.5`}
+`;
+
+const Label = styled.div`
+ ${tw`text-[#171d24] text-lg font-semibold font-['Pretendard']`}
+`;
+
+const RequiredBadge = styled.div`
+ ${tw`px-2.5 py-1.5 bg-[#a1a6b5] rounded-3xl flex justify-center items-center`}
+`;
+
+const RequiredText = styled.div`
+ ${tw`text-xs font-semibold font-['Pretendard']`}
+`;
+
+const InputContainer = styled.div`
+ ${tw`w-11/12 p-2.5 bg-[#e9eaee] rounded-lg flex justify-end items-center`}
+`;
+
+const SearchIcon = styled.div`
+ ${tw`w-6 p-px flex justify-center items-center`}
+`;
+
+const ButtonContainer = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-20 flex justify-center items-center`}
+`;
+
+export default PhotoReview;
diff --git a/src/pages/PhotoUpload/index.tsx b/src/pages/PhotoUpload/index.tsx
new file mode 100644
index 0000000..a72c028
--- /dev/null
+++ b/src/pages/PhotoUpload/index.tsx
@@ -0,0 +1,131 @@
+import { useState } from "react";
+import tw from "twin.macro";
+import styled from "styled-components";
+import { useNavigate } from "react-router-dom";
+import CloseIcon from "../../assets/icons/close-icon";
+
+type OptionProps = {
+ onClick: () => void;
+ isActive: boolean;
+ label: string;
+ subLabel?: string;
+}
+
+const OptionComponent = ({ onClick, isActive, label, subLabel }: OptionProps) => {
+ return (
+
+ );
+};
+
+function PhotoUpload() {
+ const [activeOption, setActiveOption] = useState(null);
+ const navigate = useNavigate();
+
+ const handleOptionClick = (option: string) => {
+ setActiveOption(option);
+ console.log(`${option} 버튼이 클릭되었습니다.`);
+ };
+
+ const handleNext = () => {
+ navigate("/qr-scan");
+ };
+
+ const handleClose = () => {
+ navigate("/home");
+ };
+
+ return (
+
+
+
+
+ handleOptionClick("QR 인식")}
+ isActive={activeOption === "QR 인식"}
+ label="QR 인식"
+ subLabel="QR 인식은 하루필름 매장만 가능해요"
+ />
+ handleOptionClick("내 사진첩 불러오기")}
+ isActive={activeOption === "내 사진첩 불러오기"}
+ label="내 사진첩 불러오기"
+ />
+
+ handleNext()}>
+ 다음
+
+
+ );
+}
+
+const Container = styled.div`
+ ${tw`bg-background flex flex-col items-center min-h-screen w-full max-w-[400px] m-auto`}
+ overflow-x: hidden;
+`;
+
+const OptionContainer = styled.div`
+ ${tw`flex flex-col items-center m-auto`}
+`;
+
+const Option = styled.button`
+ ${tw`w-[267px] h-[90px] rounded-lg border mb-4 cursor-pointer transition-colors duration-200`}
+ padding: ${({ isActive }) => (isActive ? "23px 12px" : "8px 12px")};
+ background-color: ${({ isActive }) => (isActive ? "#e1e0ff" : "transparent")};
+ border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")};
+ display: flex;
+ flex-direction: ${({ isActive }) => (isActive ? "column" : "row")};
+ justify-content: center;
+ align-items: center;
+ gap: ${({ isActive }) => (isActive ? "10px" : "12px")};
+`;
+
+const CircleContainer = styled.div`
+ ${tw`w-[22px] h-[22px] relative`}
+`;
+
+const CircleBorder = styled.div<{ isActive: boolean }>`
+ ${tw`absolute w-full h-full rounded-full border-2`}
+ border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")};
+`;
+
+const CircleInner = styled.div`
+ ${tw`absolute w-2.5 h-2.5 bg-[#5453ee] rounded-full`}
+ left: 6px;
+ top: 6px;
+`;
+
+const Label = styled.div<{ isActive: boolean }>`
+ ${tw`text-base font-semibold font-['Pretendard']`}
+ color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")};
+`;
+
+const SubLabel = styled.div`
+ ${tw`text-[#5453ee] text-xs font-medium font-['Pretendard']`}
+`;
+
+const ButtonContainer = styled.button`
+ ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-12 mb-[88px] flex justify-center items-center`}
+`;
+
+const CloseButton = styled.button`
+ ${tw` absolute right-[10px]`}
+`;
+
+export default PhotoUpload;
diff --git a/src/pages/QRScan/index.tsx b/src/pages/QRScan/index.tsx
new file mode 100644
index 0000000..5459fb6
--- /dev/null
+++ b/src/pages/QRScan/index.tsx
@@ -0,0 +1,149 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import tw from "twin.macro";
+import styled from "styled-components";
+import CloseIcon from "../../assets/icons/close-icon";
+import ProgressIcon from "../../assets/icons/progress-icon";
+import { Scanner } from '@yudiel/react-qr-scanner';
+
+type ScanResult = {
+ boundingBox: object;
+ cornerPoints: Array;
+ format: string;
+ rawValue: string;
+};
+
+function QRScan() {
+ const [isCaptured, setIsCaptured] = useState(false);
+ const navigate = useNavigate();
+ const [percentage, setPercentage] = useState(0);
+ const [qrLink, setQrLink] = useState(undefined);
+
+ useEffect(() => {
+ if (isCaptured) {
+ const interval = setInterval(() => {
+ setPercentage((prev) => {
+ if (prev >= 100) {
+ clearInterval(interval);
+ return 100;
+ }
+ return prev + 2;
+ });
+ }, 100);
+ return () => clearInterval(interval);
+ }
+ }, [isCaptured]);
+
+ useEffect(() => {
+ if (percentage === 100) {
+ // 100%가 화면에 렌더링된 후 페이지 이동
+ const timeout = setTimeout(() => {
+ navigate("/photo-review", {state: {qrLink}});
+ }, 500); // 약간의 지연 시간(0.5초)을 주어 100%가 표시된 후 페이지 이동
+ return () => clearTimeout(timeout);
+ }
+ }, [percentage]);
+
+ const handleClose = () => {
+ navigate("/photo-upload");
+ };
+
+ return !isCaptured ? (
+
+
+
+
+
+ 어둡고 깔끔한 배경에서 더 잘 인식해요
+
+
+
+ 문서가 빛 반사되지 않도록 주의해주세요
+
+
+
+ {
+ if (result.length > 0) {
+ setQrLink(result[0].rawValue);
+ setIsCaptured(true);
+ }
+ }}
+ />
+
+ 네모 안에 QR을 인식해주세요
+
+ ) : (
+
+ 사진을 불러오는 중...
+
+
+
+
+ );
+}
+
+// 스타일 컴포넌트
+const Container = styled.div`
+ ${tw`bg-[#00000066] flex flex-col items-center min-h-screen w-full`}
+ overflow-x: hidden;
+`;
+
+const InfoBox = styled.div`
+ ${tw`w-[280px] h-[69px] p-2.5 bg-[#d9d9d9] rounded-lg flex flex-col justify-center items-center gap-2.5 mb-6`}
+`;
+
+const InfoText = styled.div`
+ ${tw`flex items-center gap-2.5 text-sm text-center`}
+ font-family: 'Pretendard', sans-serif;
+ color: #171d24;
+`;
+
+const CameraBox = styled.div`
+ ${tw`w-[270px] h-[270px] rounded-3xl border-4 border-[#5453ee] bg-[#FFFFFF] mb-6 overflow-hidden`}
+`;
+
+const InstructionText = styled.div`
+ ${tw`text-lg font-medium`}
+ font-family: 'Pretendard', sans-serif;
+`;
+
+const CapturedContainer = styled.div`
+ ${tw`flex flex-col justify-center items-center min-h-screen w-full bg-background`}
+`;
+
+const CapturedText = styled.div`
+ ${tw`text-2xl font-semibold mb-4 `}
+ font-family: 'Pretendard', sans-serif;
+ color: #171d24;
+`;
+
+const ProgressWrapper = styled.div`
+ ${tw`p-4 bg-background rounded-lg text-center`}
+ font-family: 'Pretendard', sans-serif;
+ color: #171d24;
+`;
+
+const CloseButton = styled.button`
+ ${tw` absolute right-[10px]`}
+`;
+
+export default QRScan;
diff --git a/src/pages/Token/index.tsx b/src/pages/Token/index.tsx
new file mode 100644
index 0000000..25bd33d
--- /dev/null
+++ b/src/pages/Token/index.tsx
@@ -0,0 +1,26 @@
+import React, { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { useAuthStore } from "../../store/useAuthStore";
+import { axiosInstance } from "../../api";
+
+function Token() {
+ const navigate = useNavigate();
+ const { login } = useAuthStore();
+ useEffect(() => {
+ const fetchLogin = async () => {
+ const query = new URLSearchParams(location.search);
+ const token = query.get("accessToken");
+ // const refresh = query.get("refreshToken");
+ if (token) {
+ useAuthStore.setState({ accessToken: token });
+ login();
+ axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${token}`;
+ navigate("/home");
+ }
+ };
+ fetchLogin();
+ }, [location.search, navigate]);
+ return ;
+}
+
+export default Token;
diff --git a/src/pages/WriteReview/index.tsx b/src/pages/WriteReview/index.tsx
new file mode 100644
index 0000000..e596b0a
--- /dev/null
+++ b/src/pages/WriteReview/index.tsx
@@ -0,0 +1,27 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import Header from "../../components/Common/Header";
+import { Outlet, useNavigate } from "react-router-dom";
+
+function WriteReview() {
+ const navigate = useNavigate();
+
+ //현재 날짜 가져오기
+ const today = new Date();
+
+ return (
+
+
+ );
+}
+
+export default WriteReview;
+const Layout = styled.div`
+ ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto font-display bg-[#FFFFFF]`}
+`;
diff --git a/src/pages/WriteReview/step1.tsx b/src/pages/WriteReview/step1.tsx
new file mode 100644
index 0000000..5f8d575
--- /dev/null
+++ b/src/pages/WriteReview/step1.tsx
@@ -0,0 +1,136 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import { useState } from "react";
+import Rating from "../../components/Common/Rating";
+import InputTagSection from "../../components/WriteReview/InputTagSection";
+import { BoothTagCategories, PhotoTagCategories } from "../../data/review-tag-categories";
+import NextButton from "../../components/Common/NextButton";
+import { useNavigate, useParams } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { searchPhotoBoothName } from "../../api/booth";
+import { useAuthStore } from "../../store/useAuthStore";
+import { searchBoothFeatures, searchPhotoFeatures } from "../../api/review";
+
+function Step1() {
+ const { boothId } = useParams() as { boothId: string };
+ const navigate = useNavigate();
+ const [rate, setRate] = useState(0);
+ const [selectedBoothTags, setSelectedBoothTags] = useState([]);
+ const [selectedPhotoTags, setSelectedPhotoTags] = useState([]);
+ const { accessToken } = useAuthStore();
+ //포토부스 이름 조회 api 호출
+ const { isLoading, data: boothName } = useQuery({
+ queryKey: ["searchBoothName", boothId],
+ queryFn: () => searchPhotoBoothName(boothId, accessToken!),
+ });
+
+ const { data: BoothFeatures } = useQuery({
+ queryKey: ["searchBoothFeatures"],
+ queryFn: () => searchBoothFeatures(accessToken!),
+ });
+
+ const { data: PhotoFeatures } = useQuery({
+ queryKey: ["searchPhotoFeatures"],
+ queryFn: () => searchPhotoFeatures(accessToken!),
+ });
+ const getRatingText = () => {
+ if (rate === 0) return "별점을 선택해주세요";
+ if (rate <= 1) return "아쉬워요";
+ if (rate <= 2) return "조금 아쉬워요";
+ if (rate <= 3) return "무난해요";
+ if (rate <= 4) return "만족해요";
+ if (rate <= 5) return "완전만족해요";
+ };
+
+ const handleNextStep = () => {
+ const totalSelectedTagsLength = selectedBoothTags.length + selectedPhotoTags.length;
+ if (totalSelectedTagsLength > 5) {
+ alert("태그는 최대 5개 선택 가능합니다.");
+ return;
+ }
+ navigate(`/write-review/${boothId}/step/2`, {
+ state: {
+ rate: rate,
+ selectedBoothTags: selectedBoothTags,
+ selectedPhotoTags: selectedPhotoTags,
+ },
+ });
+ };
+
+ return (
+
+ {!isLoading && boothName && {boothName}}
+
+ {/* 별점 입력 섹션 */}
+
+
+ 필수
+
+
+
+ {getRatingText()}
+
+
+ {/* 태그 입력 섹션 */}
+
+
+ 필수
+
+ 부스에 어울리는 키워드를 골라주세요 (최대 5개)
+
+ {BoothFeatures && (
+
+ )}
+ {PhotoFeatures && (
+
+ )}
+
+
+
+ );
+}
+
+export default Step1;
+const MainWrapper = styled.main`
+ ${tw`overflow-auto w-full mt-[80px] pb-[80px] px-[16px]`}
+
+ .booth-name-box {
+ ${tw`flex items-center justify-center h-[30px] rounded-[8px] bg-gray100 font-medium text-[14px] text-gray400 px-[20px] my-[25px] mx-auto max-w-[180px]`}
+ }
+ .desc-text {
+ ${tw`font-medium text-[12px] text-gray400 mt-[5px]`}
+ }
+`;
+
+const LabelBox = styled.div`
+ ${tw`flex mt-[20px] gap-[10px]`}
+
+ .q-label {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+
+ .tag {
+ ${tw`flex items-center justify-center w-[41px] h-[26px] rounded-[24px] bg-gray300 font-semibold text-[12px] text-[#FFFFFF]`}
+ }
+`;
+
+const RatingBox = styled.div`
+ ${tw`relative flex flex-col items-center justify-center w-[300px] h-[100px] rounded-[9px] bg-main mt-[10px] mx-auto`}
+ .sub-text {
+ ${tw`font-normal text-[16px] text-[#FFFFFF] mt-[8px]`}
+ }
+`;
diff --git a/src/pages/WriteReview/step2.tsx b/src/pages/WriteReview/step2.tsx
new file mode 100644
index 0000000..9f255a7
--- /dev/null
+++ b/src/pages/WriteReview/step2.tsx
@@ -0,0 +1,130 @@
+import tw from "twin.macro";
+import styled from "styled-components";
+import NextButton from "../../components/Common/NextButton";
+import UploadImageSection from "../../components/WriteReview/UploadImageSection";
+import { useState } from "react";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
+import { submitReviewData } from "../../api/review";
+import { getPresignedUrl, uploadToS3 } from "../../api/file";
+import { useAuthStore } from "../../store/useAuthStore";
+function Step2() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { boothId } = useParams() as { boothId: string };
+ const { rate, selectedBoothTags, selectedPhotoTags } = location.state;
+ const { accessToken } = useAuthStore();
+ const [reviewText, setReviewText] = useState("");
+ const [imageFiles, setImageFiles] = useState([]); // 새로 추가된 파일들
+
+ const validateReview = (reviewText: string): boolean => {
+ if (reviewText.length > 300) {
+ alert("리뷰는 300자를 넘을 수 없습니다.");
+ return false;
+ }
+ return true;
+ };
+
+ const getUploadedFilePaths = async (imageFiles: File[], accessToken: string): Promise => {
+ const uploadPromises = imageFiles.map(async (image) => {
+ const presignedData = await getPresignedUrl("/review", image.name, accessToken);
+ if (presignedData) {
+ await uploadToS3(presignedData.url, image);
+ return presignedData.filePath;
+ }
+ return null;
+ });
+
+ const filePaths = (await Promise.all(uploadPromises)).filter(Boolean) as string[];
+ return filePaths;
+ };
+
+ const submitReview = async (
+ accessToken: string,
+ boothId: string,
+ rate: number,
+ selectedBoothTags: number[],
+ selectedPhotoTags: number[],
+ filePaths: string[],
+ reviewText: string
+ ) => {
+ const res = await submitReviewData(
+ accessToken,
+ boothId,
+ rate,
+ selectedBoothTags,
+ selectedPhotoTags,
+ filePaths,
+ reviewText
+ );
+
+ if (res && res.code === 200) {
+ navigate("/write-review/complete");
+ }
+ };
+
+ // 메인 로직
+ const handleSubmit = async () => {
+ // Step 1: 리뷰 검증
+ if (!validateReview(reviewText)) {
+ return;
+ }
+
+ // Step 2: 이미지 업로드 및 filePaths 저장
+ const filePaths = await getUploadedFilePaths(imageFiles, accessToken!);
+
+ // Step 3: 리뷰등록 api 호출
+ await submitReview(accessToken!, boothId, rate, selectedBoothTags, selectedPhotoTags, filePaths, reviewText);
+ };
+
+ return (
+
+
+ 사진을 등록해주세요
+ 선택
+
+
+
+ 부스에 대한 설명을 작성해주세요
+ 선택
+
+
+ );
+}
+
+export default Step2;
+
+const MainWrapper = styled.main`
+ ${tw`overflow-auto w-full mt-[80px] pb-[80px] px-[16px] m-auto`}
+
+ .desc-text {
+ ${tw`font-medium text-[12px] text-gray400 mt-[5px]`}
+ }
+
+ textarea {
+ ${tw`w-full h-[235px] rounded-[8px] bg-gray100 font-display font-normal text-[16px] text-gray700 resize-none p-[15px] mt-[15px]`}
+ &:placeholder {
+ ${tw`text-gray300`}
+ }
+ &:focus {
+ ${tw`outline-none`}
+ }
+ }
+
+ .text-cnt {
+ ${tw`ml-[90%] font-medium text-[12px] text-gray400`}
+ }
+`;
+const LabelBox = styled.div`
+ ${tw`flex mt-[20px] gap-[10px]`}
+
+ .q-label {
+ ${tw`font-semibold text-[18px] text-gray700`}
+ }
+
+ .tag {
+ ${tw`flex items-center justify-center w-[41px] h-[26px] rounded-[24px] bg-gray300 font-semibold text-[12px] text-[#FFFFFF]`}
+ }
+`;
diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts
new file mode 100644
index 0000000..8782ea2
--- /dev/null
+++ b/src/store/useAuthStore.ts
@@ -0,0 +1,30 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+type AuthStore = {
+ isLoggedIn: boolean;
+ login: () => void;
+ logout: () => void;
+
+ accessToken: string | null;
+ setAccessToken: (accessToken: string) => void;
+};
+
+export const useAuthStore = create()(
+ persist(
+ (set) => ({
+ isLoggedIn: false,
+ login: () => set({ isLoggedIn: true }),
+ logout: () =>
+ set({
+ isLoggedIn: false,
+ accessToken: null,
+ }),
+ accessToken: "",
+ setAccessToken: (accessToken) => set({ accessToken }),
+ }),
+ {
+ name: "user-storage",
+ }
+ )
+);
diff --git a/src/store/useBoothFilterStore.ts b/src/store/useBoothFilterStore.ts
new file mode 100644
index 0000000..2c9970e
--- /dev/null
+++ b/src/store/useBoothFilterStore.ts
@@ -0,0 +1,23 @@
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+type BoothFilterState = {
+ lat: number;
+ lng: number;
+ selectedBrands: string[] | null;
+ setLat: (lat: number) => void;
+ setLng: (lng: number) => void;
+ setSelectedBrands: (brands: string[]) => void;
+};
+
+const useBoothFilterStore = create()(
+ devtools((set, get) => ({
+ lat: 0,
+ lng: 0,
+ selectedBrands: [],
+ setLat: (lat) => set({ lat }),
+ setLng: (lng) => set({ lng }),
+ setSelectedBrands: (brands) => set({ selectedBrands: brands }),
+ }))
+);
+
+export default useBoothFilterStore;
diff --git a/tailwind.config.js b/tailwind.config.js
index 1825ec1..b8d3b10 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -7,7 +7,7 @@ export default {
theme: {
colors: {
main: "#5453EE",
- purple: "#B0B0EE",
+ purple: "#E1E0FF",
yellow: "#FCEF7B",
green: "#9DF4B6",
blue: "#7DDFF9",
diff --git a/tsconfig.json b/tsconfig.json
index c656fd1..6361aec 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,8 @@
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"jsx": "react-jsx",
- "jsxImportSource": "@emotion/react"
+ "jsxImportSource": "@emotion/react",
+ "types": ["kakao.maps.d.ts"]
},
"include": ["src/components", "custom.d.ts"]
}
diff --git a/vite.config.ts b/vite.config.ts
index 94b489a..d6dab07 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
+
export default defineConfig({
plugins: [
react({