diff --git a/.eslintrc.json b/.eslintrc.json index 1b58af5..285b1c3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,8 @@ "plugin:prettier/recommended", "prettier", "plugin:@next/next/recommended", - "plugin:storybook/recommended" + "plugin:storybook/recommended", + "plugin:@tanstack/eslint-plugin-query/recommended" ], "parser": "@typescript-eslint/parser", "ignorePatterns": ["node_modules/**", "**/dist/**", ".next/**"], @@ -27,10 +28,13 @@ "import/resolver": { "alias": { "map": [ - ["@apis", "./src/apis"], - ["@page", "./src/page"], - ["@types", "./src/types"], - ["@util", "./src/util"] + ["@", "./src"], + ["@pages", "./src/pages"], + ["@entities", "./src/entities"], + ["@views", "./src/views"], + ["@widgets", "./src/widgets"], + ["@app", "./src/app"], + ["@shared", "./src/shared"] ], "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"] } diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..2c437a4 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,55 @@ +name: Expo_Client CI + +on: + pull_request: + branches: + - 'develop' + - 'main' + +jobs: + Expo_Client_CI: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Run Next.js build + run: npm run build + + - name: Success Discord Notification + uses: sarisia/actions-status-discord@v1.11.0 + if: ${{ success() }} + with: + webhook: ${{ secrets.WEBHOOK_CI }} + title: "πŸŽ‰ Expo Client CI" + description: "CI success" + status: ${{ job.status }} + content: "<@${{ secrets.ID1 }}> <@${{ secrets.ID2 }}>\nν™•μΈν•΄μ£Όμ„Έμš”." + username: Expo Client CI bot + url: "https://github.com/School-of-Company/Expo-Client" + color: 4CAF50 + + - name: Failure notification to discord + uses: sarisia/actions-status-discord@v1.11.0 + if: failure() + with: + webhook: ${{ secrets.WEBHOOK_CI }} + title: "❌ Expo Client CI" + description: "CI failed" + content: "μ•ˆλΌ μ•ˆλ°”κΏ”μ€˜ λŒμ•„κ°€" + status: ${{ job.status }} + username: Expo Client CI bot + url: "https://github.com/School-of-Company/Expo-Client" + color: e74c3c diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 4794b59..9773cd4 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,7 +1,7 @@ import type { Preview } from '@storybook/react'; import React from 'react'; -import { pretendard } from '../src/styles/fonts'; -import '../src/styles/globals.css'; +import { pretendard } from '../src/shared/styles/fonts'; +import '../src/shared/styles/globals.css'; const preview: Preview = { parameters: { diff --git a/README.md b/README.md index e215bc4..e74d600 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,21 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +## How to Clone? -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +``` + git clone https://github.com/School-of-Company/Expo-Client.git ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More +## Contributing -To learn more about Next.js, take a look at the following resources: +- 버그 제보: [이슈 트래컀](https://github.com/School-of-Company/Expo-Client/issues)에 μ œλ³΄ν•  버그λ₯Ό μž‘μ„±ν•©λ‹ˆλ‹€. +- κΈ°λŠ₯ μ œμ•ˆ: [이슈 트래컀](https://github.com/School-of-Company/Expo-Client/issues)에 μ œμ•ˆν•˜κ³  싢은 κΈ°λŠ₯을 μž‘μ„±ν•©λ‹ˆλ‹€. +- μ½”λ“œ κΈ°μ—¬: GitHubμ—μ„œ μ½”λ“œλ₯Ό Forkν•˜κ³ , Pull Requestλ₯Ό λ³΄λƒ…λ‹ˆλ‹€. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Node Version -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +### Dependencies -## Deploy on Vercel +> [@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query) ^5.59.14
[axios](https://www.npmjs.com/package/axios) ^1.7.7
[next](https://www.npmjs.com/package/next) 14.2.15
[react](https://www.npmjs.com/package/react) ^18
[react-dom](https://www.npmjs.com/package/react-dom) ^18
[react-toastify](https://www.npmjs.com/package/react-toastify) ^10.0.6
[tailwindcss-animate](https://www.npmjs.com/package/tailwindcss-animate) ^1.0.7
[zustand](https://www.npmjs.com/package/zustand) ^5.0.0 -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +### DevDependencies -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +> [@chromatic-com/storybook](https://www.npmjs.com/package/@chromatic-com/storybook) ^1.9.0
[@storybook/addon-essentials](https://www.npmjs.com/package/@storybook/addon-essentials) ^8.3.5
[@storybook/addon-interactions](https://www.npmjs.com/package/@storybook/addon-interactions) ^8.3.5
[@storybook/addon-links](https://www.npmjs.com/package/@storybook/addon-links) ^8.3.5
[@storybook/addon-onboarding](https://www.npmjs.com/package/@storybook/addon-onboarding) ^8.3.5
[@storybook/blocks](https://www.npmjs.com/package/@storybook/blocks) ^8.3.5
[@storybook/nextjs](https://www.npmjs.com/package/@storybook/nextjs) ^8.3.5
[@storybook/react](https://www.npmjs.com/package/@storybook/react) ^8.3.5
[@storybook/test](https://www.npmjs.com/package/@storybook/test) ^8.3.5
[@types/node](https://www.npmjs.com/package/@types/node) ^20
[@types/react](https://www.npmjs.com/package/@types/react) ^18
[@types/react-dom](https://www.npmjs.com/package/@types/react-dom) ^18
[@typescript-eslint/eslint-plugin](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin) ^7.18.0
[@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser) ^7.18.0
[eslint](https://www.npmjs.com/package/eslint) ^8.57.1
[eslint-config-airbnb-typescript](https://www.npmjs.com/package/eslint-config-airbnb-typescript) ^18.0.0
[eslint-config-next](https://www.npmjs.com/package/eslint-config-next) 14.2.15
[eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier) ^9.1.0
[eslint-import-resolver-alias](https://www.npmjs.com/package/eslint-import-resolver-alias) ^1.1.2
[eslint-plugin-prettier](https://www.npmjs.com/package/eslint-plugin-prettier) ^5.2.1
[eslint-plugin-storybook](https://www.npmjs.com/package/eslint-plugin-storybook) ^0.9.0
[husky](https://www.npmjs.com/package/husky) ^9.1.6
[lint-staged](https://www.npmjs.com/package/lint-staged) ^15.2.10
[postcss](https://www.npmjs.com/package/postcss) ^8
[prettier](https://www.npmjs.com/package/prettier) ^3.3.3
[prettier-plugin-tailwindcss](https://www.npmjs.com/package/prettier-plugin-tailwindcss) ^0.6.8
[storybook](https://www.npmjs.com/package/storybook) ^8.3.5
[tailwindcss](https://www.npmjs.com/package/tailwindcss) ^3.4.1
[typescript](https://www.npmjs.com/package/typescript) ^5.4.2 diff --git a/next.config.mjs b/next.config.mjs index 4678774..f4829a1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ['mindway-bucket.s3.ap-northeast-2.amazonaws.com'], + }, +}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 7c751f1..de42db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,14 @@ "dependencies": { "@tanstack/react-query": "^5.59.14", "axios": "^1.7.7", + "class-variance-authority": "^0.7.0", "next": "14.2.15", + "qrcode.react": "^4.1.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.53.1", + "react-kakao-maps-sdk": "^1.1.27", + "react-qr-code": "^2.0.15", "react-toastify": "^10.0.6", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.0" @@ -28,6 +33,7 @@ "@storybook/nextjs": "^8.3.5", "@storybook/react": "^8.3.5", "@storybook/test": "^8.3.5", + "@tanstack/eslint-plugin-query": "^5.62.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -1930,7 +1936,6 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -4393,6 +4398,169 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.62.1", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.1.tgz", + "integrity": "sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.15.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tanstack/query-core": { "version": "5.59.13", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz", @@ -6652,6 +6820,27 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -10789,6 +10978,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", @@ -12904,7 +13099,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -12964,6 +13158,21 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==", + "license": "MIT" + }, + "node_modules/qrcode.react": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.1.0.tgz", + "integrity": "sha512-uqXVIIVD/IPgWLYxbOczCNAQw80XCM/LulYDADF+g2xDsPj5OoRwSWtIS4jGyp295wyjKstfG1qIv/I2/rNWpQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -13173,13 +13382,55 @@ "dev": true, "license": "MIT" }, + "node_modules/react-hook-form": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", + "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "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==", - "dev": true, "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-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -13341,7 +13592,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { diff --git a/package.json b/package.json index 1fe5931..9218f53 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,14 @@ "dependencies": { "@tanstack/react-query": "^5.59.14", "axios": "^1.7.7", + "class-variance-authority": "^0.7.0", "next": "14.2.15", + "qrcode.react": "^4.1.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.53.1", + "react-kakao-maps-sdk": "^1.1.27", + "react-qr-code": "^2.0.15", "react-toastify": "^10.0.6", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.0" @@ -41,6 +46,7 @@ "@storybook/nextjs": "^8.3.5", "@storybook/react": "^8.3.5", "@storybook/test": "^8.3.5", + "@tanstack/eslint-plugin-query": "^5.62.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/app/(pages)/admin/page.tsx b/src/app/(pages)/admin/page.tsx new file mode 100644 index 0000000..1e832c3 --- /dev/null +++ b/src/app/(pages)/admin/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Admin } from '@/views/admin'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/application/[id]/[type]/page.tsx b/src/app/(pages)/application/[id]/[type]/page.tsx new file mode 100644 index 0000000..6bd81af --- /dev/null +++ b/src/app/(pages)/application/[id]/[type]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Application from '@/views/application/ui/application'; + +const page = ({ params }: { params: { id: string; type: string } }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/create-exhibition/page.tsx b/src/app/(pages)/create-exhibition/page.tsx new file mode 100644 index 0000000..34c8ab6 --- /dev/null +++ b/src/app/(pages)/create-exhibition/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import CreateExhibition from '@/views/create-exhibition/ui/createExhibition'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/create-form/[expo_id]/page.tsx b/src/app/(pages)/create-form/[expo_id]/page.tsx new file mode 100644 index 0000000..f464df9 --- /dev/null +++ b/src/app/(pages)/create-form/[expo_id]/page.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { CreateForm } from '@/views/create-form'; + +const page = ({ params }: { params: { expo_id: string } }) => { + return ; +}; + +export default page; diff --git a/src/app/(pages)/expo-created/[expo_id]/page.tsx b/src/app/(pages)/expo-created/[expo_id]/page.tsx new file mode 100644 index 0000000..6ac9a55 --- /dev/null +++ b/src/app/(pages)/expo-created/[expo_id]/page.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { ExpoCreated } from '@/views/expo-created'; + +const page = ({ params }: { params: { expo_id: string } }) => { + return ; +}; + +export default page; diff --git a/src/app/(pages)/expo-detail/[id]/page.tsx b/src/app/(pages)/expo-detail/[id]/page.tsx new file mode 100644 index 0000000..5ff369a --- /dev/null +++ b/src/app/(pages)/expo-detail/[id]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ExpoDetail from '@/views/expo-detail/ui/expo-detail'; + +const page = ({ params }: { params: { id: number } }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/expo-manage/[expo_id]/page.tsx b/src/app/(pages)/expo-manage/[expo_id]/page.tsx new file mode 100644 index 0000000..c102c10 --- /dev/null +++ b/src/app/(pages)/expo-manage/[expo_id]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { ExpoManage } from '@/views/expo-manage'; + +const Page = ({ params }: { params: { expo_id: string } }) => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/app/(pages)/name-tag/[expo_id]/page.tsx b/src/app/(pages)/name-tag/[expo_id]/page.tsx new file mode 100644 index 0000000..e532738 --- /dev/null +++ b/src/app/(pages)/name-tag/[expo_id]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { NameTag } from '@/views/name-tag'; + +const page = ({ params }: { params: { expo_id: string } }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/program/[expo_id]/page.tsx b/src/app/(pages)/program/[expo_id]/page.tsx new file mode 100644 index 0000000..a6bc9c4 --- /dev/null +++ b/src/app/(pages)/program/[expo_id]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Program } from '@/views/program'; + +const Page = ({ params }: { params: { expo_id: string } }) => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/app/(pages)/program/detail/[program_id]/page.tsx b/src/app/(pages)/program/detail/[program_id]/page.tsx new file mode 100644 index 0000000..fa78bfd --- /dev/null +++ b/src/app/(pages)/program/detail/[program_id]/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { ProgramDetail } from '@/views/program-detail'; + +const page = ({ params }: { params: { program_id: number } }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/signIn/page.tsx b/src/app/(pages)/signIn/page.tsx new file mode 100644 index 0000000..df3a7e8 --- /dev/null +++ b/src/app/(pages)/signIn/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SignIn from '@/views/signIn/ui/signIn'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/signUp/page.tsx b/src/app/(pages)/signUp/page.tsx new file mode 100644 index 0000000..4a07472 --- /dev/null +++ b/src/app/(pages)/signUp/page.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SignUp from '@/views/signUp/ui/signUp'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/src/app/(pages)/sms/[id]/[authority]/page.tsx b/src/app/(pages)/sms/[id]/[authority]/page.tsx new file mode 100644 index 0000000..ed4cd19 --- /dev/null +++ b/src/app/(pages)/sms/[id]/[authority]/page.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { SMS } from '@/views/SMS'; + +const page = () => { + return ; +}; + +export default page; diff --git a/src/app/(pages)/test/page.tsx b/src/app/(pages)/test/page.tsx new file mode 100644 index 0000000..ee9c2a0 --- /dev/null +++ b/src/app/(pages)/test/page.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { QRCodeSVG } from 'qrcode.react'; +import React, { useState, useEffect } from 'react'; + +interface UserInfo { + name: string; + email: string; + number: string; +} + +const QRCodeScanner: React.FC = () => { + const [scannedData, setScannedData] = useState(''); + const [inputBuffer, setInputBuffer] = useState(''); + const [userInfo, setUserInfo] = useState(null); + const [error, setError] = useState(''); + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + setScannedData(inputBuffer); + handleScan(inputBuffer); + setInputBuffer(''); + } else { + setInputBuffer((prev) => prev + event.key); + } + }; + + window.addEventListener('keypress', handleKeyPress); + + return () => { + window.removeEventListener('keypress', handleKeyPress); + }; + }, [inputBuffer]); + + const handleScan = async (data: string) => { + setUserInfo(null); + setError(''); + + try { + const response = await fetch('/api/QRcode', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ scannedData: data }), + }); + + if (!response.ok) { + throw new Error('QR μ½”λ“œκ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'); + } + + const user: UserInfo = await response.json(); + setUserInfo(user); + } catch (error: unknown) { + if (error instanceof Error) { + setError(error.message); + } else { + setError('μ•Œ 수 μ—†λŠ” 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); + } + } + }; + + const handlePrint = () => { + const printContents = document.getElementById('printArea')?.innerHTML; + if (printContents) { + const printWindow = window.open('', '_blank'); + printWindow?.document.write(` + + + Print QR Code + + + + ${printContents} + + + `); + printWindow?.document.close(); + } + }; + + return ( +
+

μŠ€μΊ”λœ QR μ½”λ“œ 데이터:

+

{scannedData}

+
+ {userInfo && ( +
+

μ‚¬μš©μžμ΄λ¦„: {userInfo.name}

+

번호: {userInfo.number}

+

이메일: {userInfo.email}

+
+ +
+
+ )} +
+ {error &&

{error}

} + {userInfo && ( + + )} +
+ ); +}; + +export default QRCodeScanner; diff --git a/src/app/api/QRcode/route.ts b/src/app/api/QRcode/route.ts new file mode 100644 index 0000000..1a75776 --- /dev/null +++ b/src/app/api/QRcode/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + const { scannedData } = await request.json(); + + const isValid = validateQRData(scannedData); + + if (isValid) { + const userInfo = { + name: 'μ΄λ‹€ν•œ', + email: 'test1234@gmail.com', + number: '010-1234-1234', + }; + return NextResponse.json(userInfo); + } else { + return NextResponse.json({ error: 'Invalid QR code' }, { status: 400 }); + } +} + +function validateQRData(data: string) { + return data === 'MN-003533-01'; +} diff --git a/src/app/api/admin/[admin_id]/route.ts b/src/app/api/admin/[admin_id]/route.ts new file mode 100644 index 0000000..9e36e67 --- /dev/null +++ b/src/app/api/admin/[admin_id]/route.ts @@ -0,0 +1,68 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function PATCH( + request: NextRequest, + { params }: { params: { admin_id: number } }, +) { + const { admin_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.patch( + `/admin/${admin_id}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status; + const message = error.response?.data?.message || 'Unknown error'; + return NextResponse.json({ error: message }, { status: status || 500 }); + } else { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 }, + ); + } + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { admin_id: string } }, +) { + const { admin_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.delete(`/admin/${admin_id}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status; + const message = error.response?.data?.message || 'Unknown error'; + return NextResponse.json({ error: message }, { status: status || 500 }); + } else { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 }, + ); + } + } +} diff --git a/src/app/api/admin/my/route.ts b/src/app/api/admin/my/route.ts new file mode 100644 index 0000000..f2e465e --- /dev/null +++ b/src/app/api/admin/my/route.ts @@ -0,0 +1,24 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET() { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.get('/admin/my', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = + axiosError.response?.data?.message || 'admin data get failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts new file mode 100644 index 0000000..ab16a3a --- /dev/null +++ b/src/app/api/admin/route.ts @@ -0,0 +1,58 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET() { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.get('/admin', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = + axiosError.response?.data?.message || 'request signup failed'; + + return NextResponse.json({ error: message }, { status }); + } +} + +export async function DELETE() { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + if (!accessToken) { + return NextResponse.json( + { error: 'Access token not found' }, + { status: 401 }, + ); + } + + try { + await apiClient.delete('/admin', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + const response = NextResponse.json({ success: true }); + response.cookies.set('accessToken', '', { maxAge: 0 }); + response.cookies.set('refreshToken', '', { maxAge: 0 }); + return response; + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status || 500; + const message = + error.response?.data?.message || 'delete user account failed'; + return NextResponse.json({ error: message }, { status }); + } + } +} diff --git a/src/app/api/application/[expo_id]/route.ts b/src/app/api/application/[expo_id]/route.ts new file mode 100644 index 0000000..5a49a5f --- /dev/null +++ b/src/app/api/application/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { expo_id: number } }, +) { + const body = await request.json(); + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + try { + const response = await apiClient.post( + `/application/${expo_id}`, + body, + config, + ); + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/application/field/[expo_id]/route.ts b/src/app/api/application/field/[expo_id]/route.ts new file mode 100644 index 0000000..24d612e --- /dev/null +++ b/src/app/api/application/field/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { expo_id: number } }, +) { + const body = await request.json(); + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + try { + const response = await apiClient.post( + `/application/field/${expo_id}`, + body, + config, + ); + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/application/field/standard/[expo_id]/route.ts b/src/app/api/application/field/standard/[expo_id]/route.ts new file mode 100644 index 0000000..62efbc4 --- /dev/null +++ b/src/app/api/application/field/standard/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { expo_id: number } }, +) { + const body = await request.json(); + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + try { + const response = await apiClient.post( + `/application/field/standard/${expo_id}`, + body, + config, + ); + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/application/pre-standard/[expo_id]/route.ts b/src/app/api/application/pre-standard/[expo_id]/route.ts new file mode 100644 index 0000000..b2076f2 --- /dev/null +++ b/src/app/api/application/pre-standard/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { expo_id: number } }, +) { + const body = await request.json(); + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + try { + const response = await apiClient.post( + `/application/pre-standard/${expo_id}`, + body, + config, + ); + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/attendance/[expo_id]/route.ts b/src/app/api/attendance/[expo_id]/route.ts new file mode 100644 index 0000000..47a5c3f --- /dev/null +++ b/src/app/api/attendance/[expo_id]/route.ts @@ -0,0 +1,34 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function PATCH( + request: Request, + { params }: { params: { expo_id: string } }, +) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.patch( + `/attendance/${params.expo_id}`, + body, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = + axiosError.response?.data?.message || 'Attendance update failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/attendance/standard/[standardPro_id]/route.ts b/src/app/api/attendance/standard/[standardPro_id]/route.ts new file mode 100644 index 0000000..586d383 --- /dev/null +++ b/src/app/api/attendance/standard/[standardPro_id]/route.ts @@ -0,0 +1,34 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function PATCH( + request: Request, + { params }: { params: { standardPro_id: string } }, +) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.patch( + `/attendance/standard/${params.standardPro_id}`, + body, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = + axiosError.response?.data?.message || 'Attendance update failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/attendance/training/[trainingPro_id]/route.ts b/src/app/api/attendance/training/[trainingPro_id]/route.ts new file mode 100644 index 0000000..9625c72 --- /dev/null +++ b/src/app/api/attendance/training/[trainingPro_id]/route.ts @@ -0,0 +1,34 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function PATCH( + request: Request, + { params }: { params: { trainingPro_id: string } }, +) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.patch( + `/attendance/training/${params.trainingPro_id}`, + body, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = + axiosError.response?.data?.message || 'Attendance update failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..2c9c222 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,35 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function DELETE() { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + if (!accessToken) { + return NextResponse.json( + { error: 'Access token not found' }, + { status: 401 }, + ); + } + + try { + await apiClient.delete('/auth', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + const response = NextResponse.json({ success: true }); + response.cookies.set('accessToken', '', { maxAge: 0 }); + response.cookies.set('refreshToken', '', { maxAge: 0 }); + return response; + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status || 500; + const message = error.response?.data?.message || 'Logout failed'; + return NextResponse.json({ error: message }, { status }); + } + } +} diff --git a/src/app/api/auth/signin/route.ts b/src/app/api/auth/signin/route.ts new file mode 100644 index 0000000..2fe1e35 --- /dev/null +++ b/src/app/api/auth/signin/route.ts @@ -0,0 +1,45 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +interface SigninRequestBody { + nickname: string; + password: string; +} + +export async function POST(request: Request) { + const body: SigninRequestBody = await request.json(); + + try { + const response = await apiClient.post('/auth/signin', body); + + const accessTokenExpires = new Date(); + accessTokenExpires.setHours(accessTokenExpires.getHours() + 1); + + const refreshTokenExpires = new Date(response.data.refreshTokenExpiresIn); + + cookies().set('accessToken', response.data.accessToken, { + httpOnly: true, + secure: true, + expires: accessTokenExpires, + sameSite: 'strict', + }); + + cookies().set('refreshToken', response.data.refreshToken, { + httpOnly: true, + secure: true, + expires: refreshTokenExpires, + sameSite: 'strict', + }); + + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'Signin failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts new file mode 100644 index 0000000..184aa61 --- /dev/null +++ b/src/app/api/auth/signup/route.ts @@ -0,0 +1,19 @@ +import { AxiosError } from 'axios'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST(request: Request) { + const body = await request.json(); + + try { + const response = await apiClient.post('/auth', body); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'Signun failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/auth/sms/route.ts b/src/app/api/auth/sms/route.ts new file mode 100644 index 0000000..3d208af --- /dev/null +++ b/src/app/api/auth/sms/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const phoneNumber = searchParams.get('phoneNumber'); + const code = searchParams.get('code'); + + try { + const response = await apiClient.get('/sms', { + params: { phoneNumber, code }, + }); + return NextResponse.json(response.data); + } catch (error) { + return NextResponse.json( + { error: 'Code verification failed' }, + { status: 400 }, + ); + } +} +export async function POST(request: Request) { + const body = await request.json(); + + try { + const response = await apiClient.post('/sms', body); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'sms failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/auth/tokenReissue/route.ts b/src/app/api/auth/tokenReissue/route.ts new file mode 100644 index 0000000..a119793 --- /dev/null +++ b/src/app/api/auth/tokenReissue/route.ts @@ -0,0 +1,26 @@ +import { AxiosError } from 'axios'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function PATCH() { + try { + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + throw new Error('No refresh token found'); + } + + const response = await apiClient.patch('/auth', null, { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + }); + + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'Signin failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/excel/[expo_id]/route.ts b/src/app/api/excel/[expo_id]/route.ts new file mode 100644 index 0000000..e15017f --- /dev/null +++ b/src/app/api/excel/[expo_id]/route.ts @@ -0,0 +1,43 @@ +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const config: AxiosRequestConfig = { + responseType: 'arraybuffer', + headers: accessToken + ? { + Authorization: `Bearer ${accessToken}`, + } + : undefined, + }; + + try { + const response = await apiClient.get(`/excel/${expo_id}`, config); + + const headers = new Headers(); + headers.append('Content-Disposition', 'attachment; filename="export.xlsx"'); + headers.append( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + + return new NextResponse(response.data, { + status: 200, + headers: headers, + }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'Excel export failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/expo/[expo_id]/route.ts b/src/app/api/expo/[expo_id]/route.ts new file mode 100644 index 0000000..6d5379a --- /dev/null +++ b/src/app/api/expo/[expo_id]/route.ts @@ -0,0 +1,46 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + try { + const response = await apiClient.get(`/expo/${expo_id}`); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoDetail failed'; + return NextResponse.json({ error: message }, { status }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.delete(`/expo/${expo_id}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expo delete failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/expo/route.ts b/src/app/api/expo/route.ts new file mode 100644 index 0000000..9d506bc --- /dev/null +++ b/src/app/api/expo/route.ts @@ -0,0 +1,48 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET() { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.get('/expo', config); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoList failed'; + return NextResponse.json({ error: message }, { status }); + } +} + +export async function POST(request: Request) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const response = await apiClient.post('/expo', body, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/form/[expo_id]/route.ts b/src/app/api/form/[expo_id]/route.ts new file mode 100644 index 0000000..08938cc --- /dev/null +++ b/src/app/api/form/[expo_id]/route.ts @@ -0,0 +1,54 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { expo_id: number } }, +) { + const body = await request.json(); + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + try { + const response = await apiClient.post(`/form/${expo_id}`, body, config); + + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: string } }, +) { + const { expo_id } = params; + const { searchParams } = new URL(request.url); + const type = searchParams.get('type'); + + try { + const response = await apiClient.get(`/form/${expo_id}`, { + params: { type }, + }); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/image/route.ts b/src/app/api/image/route.ts new file mode 100644 index 0000000..656aa85 --- /dev/null +++ b/src/app/api/image/route.ts @@ -0,0 +1,27 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST(request: Request) { + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + try { + const formData = await request.formData(); + const response = await apiClient.post('/image', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${accessToken}`, + }, + }); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'image upload failed'; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/map/route.ts b/src/app/api/map/route.ts new file mode 100644 index 0000000..3dea38f --- /dev/null +++ b/src/app/api/map/route.ts @@ -0,0 +1,34 @@ +import axios from 'axios'; +import { NextResponse } from 'next/server'; + +const KAKAO_REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const address = searchParams.get('address'); + + if (!address) { + return NextResponse.json( + { error: 'Address parameter is required' }, + { status: 400 }, + ); + } + + const url = `https://dapi.kakao.com/v2/local/search/address.json?query=${encodeURIComponent(address)}`; + + try { + const response = await axios.get(url, { + headers: { + Authorization: `KakaoAK ${KAKAO_REST_API_KEY}`, + }, + }); + + return NextResponse.json(response.data); + } catch (error) { + console.error('Error fetching address data:', error); + return NextResponse.json( + { error: 'Failed to fetch address data' }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/participant/[expo_id]/route.ts b/src/app/api/participant/[expo_id]/route.ts new file mode 100644 index 0000000..de96b08 --- /dev/null +++ b/src/app/api/participant/[expo_id]/route.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const type = request.nextUrl.searchParams.get('type') || 'PRE'; + const encodedName = request.nextUrl.searchParams.get('name'); + const name = encodedName ? decodeURIComponent(encodedName) : null; + console.log(encodedName); + + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + params: { type, name }, + } + : {}; + + try { + const response = await apiClient.get(`/participant/${expo_id}`, config); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoDetail failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/role/route.ts b/src/app/api/role/route.ts new file mode 100644 index 0000000..47f6ded --- /dev/null +++ b/src/app/api/role/route.ts @@ -0,0 +1,7 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const role = request.headers.get('role') || 'user'; + + return NextResponse.json({ role }); +} diff --git a/src/app/api/sms/message/[expo_id]/route.ts b/src/app/api/sms/message/[expo_id]/route.ts new file mode 100644 index 0000000..fef1a49 --- /dev/null +++ b/src/app/api/sms/message/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: NextRequest, + { params }: { params: { expo_id: string } }, +) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.post( + `/sms/message/${params.expo_id}`, + body, + config, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/sms/qr/[expo_id]/route.ts b/src/app/api/sms/qr/[expo_id]/route.ts new file mode 100644 index 0000000..d99df06 --- /dev/null +++ b/src/app/api/sms/qr/[expo_id]/route.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: NextRequest, + { params }: { params: { expo_id: string } }, +) { + const body = await request.json(); + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.post( + `/sms/qr/${params.expo_id}`, + body, + config, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/standard/[expo_id]/route.ts b/src/app/api/standard/[expo_id]/route.ts new file mode 100644 index 0000000..cfb4800 --- /dev/null +++ b/src/app/api/standard/[expo_id]/route.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.get(`/standard/${expo_id}`, config); + + return NextResponse.json(response.data); + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status; + const message = error.response?.data?.message || 'Unknown error'; + return NextResponse.json({ error: message }, { status: status || 500 }); + } else { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 }, + ); + } + } +} diff --git a/src/app/api/standard/program/[expo_id]/route.ts b/src/app/api/standard/program/[expo_id]/route.ts new file mode 100644 index 0000000..2d720f0 --- /dev/null +++ b/src/app/api/standard/program/[expo_id]/route.ts @@ -0,0 +1,34 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.get( + `/standard/program/${expo_id}`, + config, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoDetail failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/trainee/[expo_id]/route.ts b/src/app/api/trainee/[expo_id]/route.ts new file mode 100644 index 0000000..fedd1ca --- /dev/null +++ b/src/app/api/trainee/[expo_id]/route.ts @@ -0,0 +1,35 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const encodedName = request.nextUrl.searchParams.get('name'); + const name = encodedName ? decodeURIComponent(encodedName) : null; + + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + params: { name }, + } + : {}; + + try { + const response = await apiClient.get(`/trainee/${expo_id}`, config); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoDetail failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/training/[expo_id]/route.ts b/src/app/api/training/[expo_id]/route.ts new file mode 100644 index 0000000..a16df22 --- /dev/null +++ b/src/app/api/training/[expo_id]/route.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.get(`/training/${expo_id}`, config); + + return NextResponse.json(response.data); + } catch (error) { + if (error instanceof AxiosError) { + const status = error.response?.status; + const message = error.response?.data?.message || 'Unknown error'; + return NextResponse.json({ error: message }, { status: status || 500 }); + } else { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 }, + ); + } + } +} diff --git a/src/app/api/training/application/[trainingPro_id]/route.ts b/src/app/api/training/application/[trainingPro_id]/route.ts new file mode 100644 index 0000000..300998f --- /dev/null +++ b/src/app/api/training/application/[trainingPro_id]/route.ts @@ -0,0 +1,38 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function POST( + request: Request, + { params }: { params: { trainingPro_id: number } }, +) { + const body = await request.json(); + const { trainingPro_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.post( + `/training/application/${trainingPro_id}`, + body, + config, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + + const status = axiosError.response?.status; + const message = axiosError.response?.data?.message; + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/training/program/[expo_id]/route.ts b/src/app/api/training/program/[expo_id]/route.ts new file mode 100644 index 0000000..bc7263f --- /dev/null +++ b/src/app/api/training/program/[expo_id]/route.ts @@ -0,0 +1,33 @@ +import { AxiosError } from 'axios'; +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { apiClient } from '@/shared/libs/apiClient'; + +export async function GET( + request: NextRequest, + { params }: { params: { expo_id: number } }, +) { + const { expo_id } = params; + const cookieStore = cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + const config = accessToken + ? { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + : {}; + + try { + const response = await apiClient.get( + `/training/program/${expo_id}`, + config, + ); + return NextResponse.json(response.data); + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status || 500; + const message = axiosError.response?.data?.message || 'expoDetail failed'; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e467d5a..091f555 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,8 @@ -import TanstackProviders from '@/libs/TanstackProviders'; -import ToastProvider from '@/libs/ToastProvider'; -import { pretendard } from '@/styles/fonts'; -import '../styles/globals.css'; +import '../shared/styles/globals.css'; import 'react-toastify/dist/ReactToastify.css'; +import TanstackProviders from '@/shared/libs/TanstackProviders'; +import ToastProvider from '@/shared/libs/ToastProvider'; +import { pretendard } from '@/shared/styles/fonts'; export default function RootLayout({ children, diff --git a/src/app/page.tsx b/src/app/page.tsx index 8c01ef9..b005285 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,5 @@ -'use client'; - -import { toast } from 'react-toastify'; -import Counter from '@/components/Counter'; +import Main from '@/views/main/ui/main'; export default function Home() { - const handleClick = () => { - toast.success('ν•˜μ΄'); - }; - - return ( -
-

Zustand Counter ν…ŒμŠ€νŠΈ ν•˜μ΄

- - -
- ); + return
; } diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx deleted file mode 100644 index 8fa497e..0000000 --- a/src/components/Counter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import React from 'react'; -import useStore from '@/stores/useStore'; - -const Counter: React.FC = () => { - const { count, increment, decrement } = useStore(); - - return ( -
-

Count: {count}

- - -
- ); -}; - -export default Counter; diff --git a/src/components/button/index.stories.ts b/src/components/button/index.stories.ts deleted file mode 100644 index 9b53edc..0000000 --- a/src/components/button/index.stories.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import Button from '.'; - -const meta = { - title: 'Button', - component: Button, - tags: ['autodocs'], - argTypes: { - text: { control: 'text' }, - }, - parameters: { - layout: 'centered', - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; diff --git a/src/components/button/index.tsx b/src/components/button/index.tsx deleted file mode 100644 index 06385c2..0000000 --- a/src/components/button/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const Button = () => { - return ; -}; - -export default Button; diff --git a/src/entities/Etc/index.tsx b/src/entities/Etc/index.tsx new file mode 100644 index 0000000..4beb1ba --- /dev/null +++ b/src/entities/Etc/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const Etc = () => { + return ( +
+

기타

+

(μ§μ ‘μž…λ ₯)

+
+ ); +}; + +export default Etc; diff --git a/src/entities/admin/api/deleteLogout.ts b/src/entities/admin/api/deleteLogout.ts new file mode 100644 index 0000000..703fdba --- /dev/null +++ b/src/entities/admin/api/deleteLogout.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; + +export const deleteLogout = async () => { + const response = await axios.delete('/api/auth/logout'); + return response; +}; diff --git a/src/entities/admin/api/deleteUserAccount.ts b/src/entities/admin/api/deleteUserAccount.ts new file mode 100644 index 0000000..ed4f037 --- /dev/null +++ b/src/entities/admin/api/deleteUserAccount.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; + +export const deleteUserAccount = async () => { + const response = await axios.delete('/api/admin'); + return response; +}; diff --git a/src/entities/admin/index.tsx b/src/entities/admin/index.tsx new file mode 100644 index 0000000..4edb65d --- /dev/null +++ b/src/entities/admin/index.tsx @@ -0,0 +1 @@ +export { default as AdminProfile } from './ui/AdminProfile'; diff --git a/src/entities/admin/model/useDeleteUserAccount.ts b/src/entities/admin/model/useDeleteUserAccount.ts new file mode 100644 index 0000000..f11745c --- /dev/null +++ b/src/entities/admin/model/useDeleteUserAccount.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { toast } from 'react-toastify'; +import { deleteUserAccount } from '../api/deleteUserAccount'; + +export const useDeleteUserAccount = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: () => deleteUserAccount(), + onSuccess: () => { + toast.success('νƒˆν‡΄κ°€ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + router.push('/'); + }, + onError: () => { + toast.error('μœ μ € νƒˆν‡΄λ₯Ό μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); + }, + }); +}; diff --git a/src/entities/admin/model/useLogout.ts b/src/entities/admin/model/useLogout.ts new file mode 100644 index 0000000..5ee1357 --- /dev/null +++ b/src/entities/admin/model/useLogout.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { toast } from 'react-toastify'; +import { deleteLogout } from '../api/deleteLogout'; + +export const useLogout = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: () => deleteLogout(), + onSuccess: () => { + toast.success('λ‘œκ·Έμ•„μ›ƒμ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + router.push('/'); + }, + onError: () => { + toast.error('λ‘œκ·Έμ•„μ›ƒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); + }, + }); +}; diff --git a/src/entities/admin/ui/AdminProfile/index.tsx b/src/entities/admin/ui/AdminProfile/index.tsx new file mode 100644 index 0000000..d30ade8 --- /dev/null +++ b/src/entities/admin/ui/AdminProfile/index.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { Logout } from '@/shared/assets/icons'; +import { AdminData } from '@/shared/types/admin/type'; +import { useDeleteUserAccount } from '../../model/useDeleteUserAccount'; +import { useLogout } from '../../model/useLogout'; + +const ProfileInfo = ({ label, value }: { label: string; value: string }) => ( +
+

{label}

+

{value}

+
+); + +const AdminProfile = ({ data }: { data: AdminData }) => { + const { mutate: logout } = useLogout(); + const { mutate: deleteAccount } = useDeleteUserAccount(); + const [isToggleLogout, setIsToggleLogout] = useState(false); + + const handleLogoutClick = () => { + setIsToggleLogout((prev) => !prev); + }; + + return ( +
+
+
+ + + +
+
+
+ + {isToggleLogout && ( +
+ + +
+ )} +
+
+ ); +}; + +export default AdminProfile; diff --git a/src/entities/application/ui/CheckBoxOption/index.tsx b/src/entities/application/ui/CheckBoxOption/index.tsx new file mode 100644 index 0000000..d550545 --- /dev/null +++ b/src/entities/application/ui/CheckBoxOption/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { UseFormRegister, UseFormWatch } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; +import EtcOption from '../EtcOption'; + +interface Option { + value: string; + label: string; +} + +interface Props { + options: Option[]; + register: UseFormRegister; + watch: UseFormWatch; + name: string; + required: boolean; + otherJson: string | null; +} + +const CheckBoxOption = ({ + options, + register, + watch, + name, + required, + otherJson, +}: Props) => { + return ( +
+ {options.map((option) => ( +
+ + +
+ ))} + {otherJson !== null && ( + + )} +
+ ); +}; + +export default CheckBoxOption; diff --git a/src/entities/application/ui/DropDownOption/index.tsx b/src/entities/application/ui/DropDownOption/index.tsx new file mode 100644 index 0000000..9103cf7 --- /dev/null +++ b/src/entities/application/ui/DropDownOption/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { UseFormRegister } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; + +interface Option { + value: string; + label: string; +} + +interface Props { + options: Option[]; + register: UseFormRegister; + name: string; + required: boolean; +} + +const DropDownOption = ({ options, register, name, required }: Props) => { + return ( + + ); +}; + +export default DropDownOption; diff --git a/src/entities/application/ui/EtcOption/index.tsx b/src/entities/application/ui/EtcOption/index.tsx new file mode 100644 index 0000000..751ba00 --- /dev/null +++ b/src/entities/application/ui/EtcOption/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { UseFormRegister, UseFormWatch } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; + +interface Props { + type: 'radio' | 'checkbox'; + register: UseFormRegister; + watch: UseFormWatch; + name: string; +} + +const EtcOption = ({ type, register, watch, name }: Props) => { + const watchedValue = watch(name); + const isEtcSelected = Array.isArray(watchedValue) + ? watchedValue.includes('etc') + : watchedValue === 'etc'; + + return ( +
+ + + +
+ ); +}; + +export default EtcOption; diff --git a/src/entities/application/ui/MultipleOption/index.tsx b/src/entities/application/ui/MultipleOption/index.tsx new file mode 100644 index 0000000..4745ff9 --- /dev/null +++ b/src/entities/application/ui/MultipleOption/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { UseFormRegister, UseFormWatch } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; +import EtcOption from '../EtcOption'; + +interface Option { + value: string; + label: string; +} + +interface Props { + options: Option[]; + register: UseFormRegister; + watch: UseFormWatch; + name: string; + required: boolean; + otherJson: string | null; +} + +const MultipleOption = ({ + options, + register, + watch, + name, + required, + otherJson, +}: Props) => { + return ( +
+ {options.map((option) => ( +
+ + +
+ ))} + {otherJson !== null && ( + + )} +
+ ); +}; + +export default MultipleOption; diff --git a/src/entities/application/ui/OptionContainer/index.tsx b/src/entities/application/ui/OptionContainer/index.tsx new file mode 100644 index 0000000..71dad70 --- /dev/null +++ b/src/entities/application/ui/OptionContainer/index.tsx @@ -0,0 +1,93 @@ +import { UseFormRegister, UseFormWatch } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; +import CheckBoxOption from '../CheckBoxOption'; +import DropDownOption from '../DropDownOption'; +import MultipleOption from '../MultipleOption'; +import SentenceOption from '../SentenceOption'; + +const OptionContainer = ({ + title, + formType, + jsonData, + requiredStatus, + otherJson, + register, + watch, +}: { + title: string; + formType: string; + jsonData?: string; + requiredStatus: boolean; + otherJson: string | null; + register: UseFormRegister; + watch: UseFormWatch; +}) => { + const options = jsonData + ? Object.entries(JSON.parse(jsonData)).map(([key, value]) => ({ + value: key, + label: value as string, + })) + : []; + + let inputComponent; + switch (formType) { + case 'SENTENCE': + inputComponent = ( + + ); + break; + case 'CHECKBOX': + inputComponent = ( + + ); + break; + case 'MULTIPLE': + inputComponent = ( + + ); + break; + case 'DROPDOWN': + inputComponent = ( + + ); + break; + } + + return ( +
+
+

{title}

+ {requiredStatus ?

*

: null} +
+ +
{inputComponent}
+
+ ); +}; + +export default OptionContainer; diff --git a/src/entities/application/ui/SentenceOption/index.tsx b/src/entities/application/ui/SentenceOption/index.tsx new file mode 100644 index 0000000..21015d2 --- /dev/null +++ b/src/entities/application/ui/SentenceOption/index.tsx @@ -0,0 +1,55 @@ +import React, { useRef } from 'react'; +import { UseFormRegister } from 'react-hook-form'; +import { ApplicationFormValues } from '@/shared/types/application/type'; + +interface Props { + maxLength: number; + row: number; + required: boolean; + register: UseFormRegister; + name: string; +} + +export default function SentenceOption({ + maxLength, + row, + required, + register, + name, +}: Props) { + const { ref, onChange, ...rest } = register(name, { + required: required ? 'ν•„μˆ˜ μ˜΅μ…˜μ„ μž‘μ„±ν•΄μ£Όμ„Έμš”' : false, + }); + const textareaRef = useRef(null); + + const handleChange = (e: React.ChangeEvent) => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + + if (onChange) { + onChange(e); + } + }; + + return ( +
+
+