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 ( + + + + console.error("Image load error:", e)} // 이미지 로딩 에러 확인 + /> + + + + + + ); +}; + +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 ( + + + + console.error("Image load error:", e)} // 이미지 로딩 에러 확인 + /> + + + + + + ); +}; + +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) => ( + + +
+ tag img + {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) => ( +
+
    {dots}
+
+ ), + 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 ( + + {/* 사용자 위치를 나타내는 원형 마커 */} + + {/* 방향을 나타내는 화살표 마커 */} + {/* {position.heading !== null && ( + */} + {/* )} */} + {children} + + ); +} + +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일 +
+
+ + 하루필름 건대입구역점 +
+
+ + 4.5 +
+
+
+ ); +} + +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) => ( + + {`Uploaded + 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 ? ( +