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 }) => (
+
+);
+
+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 (
+
+ );
+}
diff --git a/src/entities/create-exhibition/index.tsx b/src/entities/create-exhibition/index.tsx
new file mode 100644
index 0000000..ba84ba0
--- /dev/null
+++ b/src/entities/create-exhibition/index.tsx
@@ -0,0 +1,3 @@
+export { default as ExpoInput } from './ui/ExpoInput';
+export { default as ImageInput } from './ui/ImageInput';
+export { default as TrainingModule } from './ui/TrainingModule';
diff --git a/src/entities/create-exhibition/model/useTrainingInputs.ts b/src/entities/create-exhibition/model/useTrainingInputs.ts
new file mode 100644
index 0000000..50e6bdb
--- /dev/null
+++ b/src/entities/create-exhibition/model/useTrainingInputs.ts
@@ -0,0 +1,23 @@
+import { useState } from 'react';
+
+export const useTrainingInputs = () => {
+ const [inputs, setInputs] = useState([]);
+
+ const addInput = (): void => {
+ setInputs([...inputs, '']);
+ };
+
+ const removeInput = (index: number): void => {
+ const newInput = [...inputs];
+ newInput.splice(index, 1);
+ setInputs(newInput);
+ };
+
+ const onChangeInput = (index: number, value: string): void => {
+ const newInput = [...inputs];
+ newInput[index] = value;
+ setInputs(newInput);
+ };
+
+ return { inputs, addInput, removeInput, onChangeInput };
+};
diff --git a/src/entities/create-exhibition/ui/ExpoInput/index.tsx b/src/entities/create-exhibition/ui/ExpoInput/index.tsx
new file mode 100644
index 0000000..b0556aa
--- /dev/null
+++ b/src/entities/create-exhibition/ui/ExpoInput/index.tsx
@@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { XMark } from '@/shared/assets/icons';
+import { FieldArrayProps } from '@/shared/types/create-exhibition/type';
+import { AddItemButton } from '@/shared/ui';
+import Modal from '../Modal';
+
+const ExpoInput = ({
+ fields,
+ append,
+ remove,
+ register,
+ setValue,
+ watch,
+ fieldName,
+}: FieldArrayProps) => {
+ const [modal, setModal] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(null);
+
+ const handleTrainingModal = (index: number) => {
+ setSelectedIndex(index);
+ setModal(true);
+ };
+
+ const handleRemove = (index: number) => {
+ remove(index);
+ setSelectedIndex(null);
+ setModal(false);
+ };
+
+ const items = watch(fieldName);
+
+ return (
+
+ {modal && selectedIndex !== null && (
+
+ )}
+ {fields.length > 0 && (
+
+ {fields.map((field, index) => (
+
+
+
{index + 1}
+
+ setValue(
+ `${fieldName}.${index}.title` as const,
+ e.target.value,
+ )
+ }
+ />
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ append({
+ title: '',
+ startedAt: '',
+ endedAt: '',
+ ...(fieldName === 'trainings' ? { category: 'CHOICE' } : {}),
+ })
+ }
+ />
+
+ );
+};
+
+export default ExpoInput;
diff --git a/src/entities/create-exhibition/ui/ImageInput/index.tsx b/src/entities/create-exhibition/ui/ImageInput/index.tsx
new file mode 100644
index 0000000..b5e682a
--- /dev/null
+++ b/src/entities/create-exhibition/ui/ImageInput/index.tsx
@@ -0,0 +1,74 @@
+import Image from 'next/image';
+import React, { useState } from 'react';
+import { UseFormRegisterReturn, UseFormSetValue } from 'react-hook-form';
+import { toast } from 'react-toastify';
+import { Picture } from '@/shared/assets/icons';
+import { ExhibitionFormData } from '@/widgets/create-exhibition/types/type';
+import WarningMessage from '../WarningMessage';
+
+interface ImageInputProps {
+ register: UseFormRegisterReturn;
+ setValue: UseFormSetValue;
+ id: string;
+}
+
+const ImageInput = ({ register, setValue }: ImageInputProps) => {
+ const [img, setImg] = useState(null);
+
+ const handleImageChange = (e: React.ChangeEvent): void => {
+ const file = e.target.files?.[0];
+
+ if (file) {
+ const imgElement = new window.Image();
+ imgElement.src = URL.createObjectURL(file);
+
+ imgElement.onload = () => {
+ if (imgElement.width < 750 || imgElement.height < 360) {
+ toast.error('μ΄λ―Έμ§μ μ΅μ ν¬κΈ°λ 750x360μ΄μ΄μΌ ν©λλ€.');
+ return;
+ }
+
+ setImg(URL.createObjectURL(file));
+ setValue('image', file);
+ };
+
+ register.onChange(e);
+ }
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default ImageInput;
diff --git a/src/entities/create-exhibition/ui/Modal/index.tsx b/src/entities/create-exhibition/ui/Modal/index.tsx
new file mode 100644
index 0000000..b428623
--- /dev/null
+++ b/src/entities/create-exhibition/ui/Modal/index.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { XMark } from '@/shared/assets/icons';
+import { ModalProps } from '@/shared/types/create-exhibition/type';
+import { Button, CheckBox, Input } from '@/shared/ui';
+
+const Modal = ({ setModal, setValue, watch, index, fieldName }: ModalProps) => {
+ const startedAt = watch(`${fieldName}.${index}.startedAt`);
+ const endedAt = watch(`${fieldName}.${index}.endedAt`);
+
+ return (
+
+
+
μ°μ μ€μ
+
+
+
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/entities/create-exhibition/ui/TrainingModule/index.tsx b/src/entities/create-exhibition/ui/TrainingModule/index.tsx
new file mode 100644
index 0000000..fd8cb8e
--- /dev/null
+++ b/src/entities/create-exhibition/ui/TrainingModule/index.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { FieldArrayProps } from '@/shared/types/create-exhibition/type';
+import ExpoInput from '../ExpoInput';
+
+const TrainingModule = ({
+ fields,
+ append,
+ remove,
+ register,
+ setValue,
+ watch,
+ fieldName,
+}: FieldArrayProps) => {
+ const createEmptyField = () => {
+ if (fieldName === 'trainings') {
+ return { title: '', startedAt: '', endedAt: '', category: 'CHOICE' };
+ }
+ return { title: '', startedAt: '', endedAt: '' };
+ };
+
+ return (
+
+
+ append(createEmptyField())}
+ remove={remove}
+ register={register}
+ setValue={setValue}
+ watch={watch}
+ fieldName={fieldName}
+ />
+
+
+ );
+};
+
+export default TrainingModule;
diff --git a/src/entities/create-exhibition/ui/WarningMessage/index.tsx b/src/entities/create-exhibition/ui/WarningMessage/index.tsx
new file mode 100644
index 0000000..5313769
--- /dev/null
+++ b/src/entities/create-exhibition/ui/WarningMessage/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Warning } from '@/shared/assets/icons';
+
+const WarningMessage = ({ text }: { text: string }) => {
+ return (
+
+ );
+};
+
+export default WarningMessage;
diff --git a/src/entities/create-form/index.tsx b/src/entities/create-form/index.tsx
new file mode 100644
index 0000000..b0e6524
--- /dev/null
+++ b/src/entities/create-form/index.tsx
@@ -0,0 +1,10 @@
+export { default as CheckBoxOption } from './ui/CheckBoxOption';
+export { default as DeleteButton } from './ui/DeleteButton';
+export { default as DropDownOption } from './ui/DropDownOption';
+export { default as FormTitle } from './ui/FormTitle';
+export { default as FormTypeSelect } from './ui/FormTypeSelect';
+export { default as MultipleChoiceOption } from './ui/MultipleChoiceOption';
+export { default as PictureOption } from './ui/PictureOption';
+export { default as RequiredToggle } from './ui/RequiredToggle';
+export { default as CreateFormButton } from './ui/CreateFormButton';
+export { default as CheckBox } from './ui/CheckBox';
diff --git a/src/entities/create-form/ui/CheckBox/index.tsx b/src/entities/create-form/ui/CheckBox/index.tsx
new file mode 100644
index 0000000..dc38e86
--- /dev/null
+++ b/src/entities/create-form/ui/CheckBox/index.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Control, useController } from 'react-hook-form';
+import { Check } from '@/shared/assets/icons';
+import { FormValues } from '@/shared/types/create-form/type';
+
+interface Props {
+ control: Control;
+ index: number;
+ text: string;
+}
+
+const CheckBox = ({ control, index, text }: Props) => {
+ const { field } = useController({
+ name: `questions.${index}.otherJson`,
+ control,
+ defaultValue: null,
+ });
+
+ const toggleCheck = () => {
+ field.onChange(field.value ? null : 'etc');
+ };
+
+ return (
+
+ );
+};
+
+export default CheckBox;
diff --git a/src/entities/create-form/ui/CheckBoxOption/index.tsx b/src/entities/create-form/ui/CheckBoxOption/index.tsx
new file mode 100644
index 0000000..ddae026
--- /dev/null
+++ b/src/entities/create-form/ui/CheckBoxOption/index.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Square } from '@/shared/assets/svg';
+import { OptionProps } from '@/shared/types/create-form/type';
+import OptionItem from '../OptionItem';
+import OtherOption from '../OtherOptionProps';
+
+const CheckBoxOption = ({
+ fields,
+ remove,
+ register,
+ index,
+ isCheckBox,
+}: OptionProps) => {
+ return (
+
+ {fields.map((option, optionIndex) => (
+ }
+ optionId={option.id}
+ optionIndex={optionIndex}
+ register={register}
+ remove={remove}
+ inputName={`questions.${index}.options.${optionIndex}.value`}
+ />
+ ))}
+ {isCheckBox ? : null}
+
+ );
+};
+
+export default CheckBoxOption;
diff --git a/src/entities/create-form/ui/CreateFormButton/index.tsx b/src/entities/create-form/ui/CreateFormButton/index.tsx
new file mode 100644
index 0000000..5e7bae0
--- /dev/null
+++ b/src/entities/create-form/ui/CreateFormButton/index.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Plus } from '@/shared/assets/icons';
+
+interface Props {
+ onClick?: React.MouseEventHandler;
+}
+
+const CreateFormButton = ({ onClick }: Props) => {
+ return (
+
+ );
+};
+
+export default CreateFormButton;
diff --git a/src/entities/create-form/ui/DeleteButton/index.tsx b/src/entities/create-form/ui/DeleteButton/index.tsx
new file mode 100644
index 0000000..a3a1b47
--- /dev/null
+++ b/src/entities/create-form/ui/DeleteButton/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Trash } from '@/shared/assets/icons';
+
+interface Props {
+ onClick: (e: React.MouseEvent) => void;
+}
+
+const DeleteButton = ({ onClick }: Props) => {
+ return (
+
+ );
+};
+
+export default DeleteButton;
diff --git a/src/entities/create-form/ui/DropDownOption/index.tsx b/src/entities/create-form/ui/DropDownOption/index.tsx
new file mode 100644
index 0000000..0fd150f
--- /dev/null
+++ b/src/entities/create-form/ui/DropDownOption/index.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { OptionProps } from '@/shared/types/create-form/type';
+import OptionItem from '../OptionItem';
+import OtherOption from '../OtherOptionProps';
+
+const DropDownOption = ({
+ fields,
+ remove,
+ register,
+ index,
+ isCheckBox,
+}: OptionProps) => {
+ return (
+
+ {fields.map((option, optionIndex) => (
+
+ ))}
+ {isCheckBox ? : null}
+
+ );
+};
+
+export default DropDownOption;
diff --git a/src/entities/create-form/ui/FormTitle/index.tsx b/src/entities/create-form/ui/FormTitle/index.tsx
new file mode 100644
index 0000000..69c895f
--- /dev/null
+++ b/src/entities/create-form/ui/FormTitle/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { UseFormRegister } from 'react-hook-form';
+import { FormValues } from '@/shared/types/create-form/type';
+
+interface Props {
+ index: number;
+ register: UseFormRegister;
+}
+
+const FormTitle = ({ register, index }: Props) => {
+ return (
+
+
+
+ );
+};
+
+export default FormTitle;
diff --git a/src/entities/create-form/ui/FormTypeSelect/index.tsx b/src/entities/create-form/ui/FormTypeSelect/index.tsx
new file mode 100644
index 0000000..be5420d
--- /dev/null
+++ b/src/entities/create-form/ui/FormTypeSelect/index.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
+import { ArrowDown, ArrowUp } from '@/shared/assets/icons';
+import { preventEvent } from '@/shared/model/preventEvent';
+import { FormValues, Option } from '@/shared/types/create-form/type';
+
+interface Props {
+ options: Option[];
+ selectedOption: Option | null;
+ setSelectedOption: (option: Option) => void;
+ register: UseFormRegister;
+ index: number;
+ setValue: UseFormSetValue;
+}
+
+const FormTypeSelect = ({
+ options,
+ selectedOption,
+ setSelectedOption,
+ register,
+ index,
+ setValue,
+}: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ if (selectedOption) {
+ setValue(`questions.${index}.formType`, selectedOption.value);
+ }
+ }, [selectedOption, setValue, index]);
+
+ return (
+
+
+
+ {isOpen && (
+
+ {options.map((option) => (
+ - {
+ preventEvent(e);
+ setSelectedOption(option);
+ setValue(`questions.${index}.formType`, option.value);
+ setIsOpen(false);
+ }}
+ className="flex w-full cursor-pointer items-center justify-center p-2 text-h5 text-gray-500 hover:bg-gray-100"
+ >
+ {option.icon && {option.icon}}
+ {option.label}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default FormTypeSelect;
diff --git a/src/entities/create-form/ui/MultipleChoiceOption/index.tsx b/src/entities/create-form/ui/MultipleChoiceOption/index.tsx
new file mode 100644
index 0000000..32cb2cf
--- /dev/null
+++ b/src/entities/create-form/ui/MultipleChoiceOption/index.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Circle } from '@/shared/assets/svg';
+import { OptionProps } from '@/shared/types/create-form/type';
+import OptionItem from '../OptionItem';
+import OtherOption from '../OtherOptionProps';
+
+const MultipleChoiceOption = ({
+ fields,
+ remove,
+ register,
+ index,
+ isCheckBox,
+}: OptionProps) => {
+ return (
+
+ {fields.map((option, optionIndex) => (
+ }
+ optionId={option.id}
+ optionIndex={optionIndex}
+ register={register}
+ remove={remove}
+ inputName={`questions.${index}.options.${optionIndex}.value`}
+ />
+ ))}
+ {isCheckBox ? : null}
+
+ );
+};
+
+export default MultipleChoiceOption;
diff --git a/src/entities/create-form/ui/OptionItem/index.tsx b/src/entities/create-form/ui/OptionItem/index.tsx
new file mode 100644
index 0000000..2e47745
--- /dev/null
+++ b/src/entities/create-form/ui/OptionItem/index.tsx
@@ -0,0 +1,40 @@
+import React, { ReactNode } from 'react';
+import { UseFormRegister } from 'react-hook-form';
+import { XMark } from '@/shared/assets/icons';
+import { FormValues } from '@/shared/types/create-form/type';
+
+interface OptionItemProps {
+ optionId: string;
+ icon?: ReactNode;
+ optionIndex: number;
+ register: UseFormRegister;
+ remove: (index: number) => void;
+ inputName: `questions.${number}.options.${number}.value`;
+}
+
+const OptionItem = ({
+ optionId,
+ icon,
+ optionIndex,
+ register,
+ remove,
+ inputName,
+}: OptionItemProps) => {
+ return (
+
+
+ {icon}
+
+
+
+
+ );
+};
+
+export default OptionItem;
diff --git a/src/entities/create-form/ui/OtherOptionProps/index.tsx b/src/entities/create-form/ui/OtherOptionProps/index.tsx
new file mode 100644
index 0000000..de46a6b
--- /dev/null
+++ b/src/entities/create-form/ui/OtherOptionProps/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+interface Props {
+ icon?: React.ReactNode;
+ text: string;
+}
+
+const OtherOption = ({ icon, text }: Props) => {
+ return (
+
+ {icon}
+
{text}
+
(μ§μ μ
λ ₯)
+
+ );
+};
+
+export default OtherOption;
diff --git a/src/entities/create-form/ui/PictureOption/index.tsx b/src/entities/create-form/ui/PictureOption/index.tsx
new file mode 100644
index 0000000..0f0ab7d
--- /dev/null
+++ b/src/entities/create-form/ui/PictureOption/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { FormPicture } from '@/shared/assets/icons';
+import { OptionProps } from '@/shared/types/create-form/type';
+import OptionItem from '../OptionItem';
+
+const PictureOption = ({ fields, remove, register, index }: OptionProps) => {
+ return (
+
+ {fields.map((option, optionIndex) => (
+ }
+ optionId={option.id}
+ optionIndex={optionIndex}
+ register={register}
+ remove={remove}
+ inputName={`questions.${index}.options.${optionIndex}.value`}
+ />
+ ))}
+
+ );
+};
+
+export default PictureOption;
diff --git a/src/entities/create-form/ui/RequiredToggle/index.tsx b/src/entities/create-form/ui/RequiredToggle/index.tsx
new file mode 100644
index 0000000..6568b4c
--- /dev/null
+++ b/src/entities/create-form/ui/RequiredToggle/index.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Control, useController } from 'react-hook-form';
+import { FormValues } from '@/shared/types/create-form/type';
+import ToggleButton from '@/shared/ui/ToggleButton';
+
+interface Props {
+ control: Control;
+ index: number;
+}
+
+const RequiredToggle = ({ control, index }: Props) => {
+ const { field } = useController({
+ name: `questions.${index}.requiredStatus`,
+ control,
+ defaultValue: false,
+ });
+
+ return (
+
+ );
+};
+
+export default RequiredToggle;
diff --git a/src/entities/expo-detail/index.tsx b/src/entities/expo-detail/index.tsx
new file mode 100644
index 0000000..bfce354
--- /dev/null
+++ b/src/entities/expo-detail/index.tsx
@@ -0,0 +1,5 @@
+export { default as ContentText } from './ui/ContentText';
+export { default as DetailHeader } from './ui/DetailHeader';
+export { default as ExpoActionPanel } from '../../widgets/expo-detail/ui/ExpoActionPanel';
+export { default as KaKaoMap } from './ui/KaKaoMap';
+export { default as QRcode } from './ui/QRcode';
diff --git a/src/entities/expo-detail/model/useKakaoMap.ts b/src/entities/expo-detail/model/useKakaoMap.ts
new file mode 100644
index 0000000..85e3e51
--- /dev/null
+++ b/src/entities/expo-detail/model/useKakaoMap.ts
@@ -0,0 +1,50 @@
+import { useEffect, useRef } from 'react';
+
+interface KakaoMapOptions {
+ latitude: number;
+ longitude: number;
+}
+
+export const useKakaoMap = ({ latitude, longitude }: KakaoMapOptions) => {
+ const mapRef = useRef(null);
+
+ const loadKakaoMap = () => {
+ if (!window.kakao || !mapRef.current) return;
+
+ const position = new window.kakao.maps.LatLng(latitude, longitude);
+
+ const options = {
+ center: position,
+ level: 3,
+ };
+
+ const map = new window.kakao.maps.Map(mapRef.current, options);
+
+ const marker = new window.kakao.maps.Marker({
+ position: position,
+ });
+
+ marker.setMap(map);
+ };
+
+ useEffect(() => {
+ const initializeMap = () => {
+ if (typeof window.kakao !== 'undefined') {
+ window.kakao.maps.load(() => {
+ loadKakaoMap();
+ });
+ } else {
+ console.error('Kakao Maps APIκ° λ‘λλμ§ μμμ΅λλ€.');
+ }
+ };
+
+ if (document.readyState === 'complete') {
+ initializeMap();
+ } else {
+ window.addEventListener('load', initializeMap);
+ return () => window.removeEventListener('load', initializeMap);
+ }
+ }, [latitude, longitude]);
+
+ return { mapRef, loadKakaoMap };
+};
diff --git a/src/entities/expo-detail/ui/ContentListText/index.tsx b/src/entities/expo-detail/ui/ContentListText/index.tsx
new file mode 100644
index 0000000..bcce39c
--- /dev/null
+++ b/src/entities/expo-detail/ui/ContentListText/index.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+interface ExpoStandard {
+ title: string;
+ startedAt: string;
+ endedAt: string;
+}
+
+interface Props {
+ data: ExpoStandard[];
+ title: string;
+}
+
+const ContentListText = ({ data, title }: Props) => {
+ // dataκ° μμΌλ©΄ λΉ λ°°μ΄λ‘ μ²λ¦¬
+ const validData = data || [];
+
+ return (
+
+
{title}
+ {validData.map((item, index) => (
+
+
- {item.title}
+
+ ({item.startedAt} ~ {item.endedAt})
+
+
+ ))}
+
+ );
+};
+
+export default ContentListText;
diff --git a/src/entities/expo-detail/ui/ContentText/index.tsx b/src/entities/expo-detail/ui/ContentText/index.tsx
new file mode 100644
index 0000000..ee847ee
--- /dev/null
+++ b/src/entities/expo-detail/ui/ContentText/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+interface Props {
+ title: string;
+ content: string;
+}
+
+const ContentText = ({ title, content }: Props) => {
+ return (
+
+ );
+};
+
+export default ContentText;
diff --git a/src/entities/expo-detail/ui/DetailHeader/index.tsx b/src/entities/expo-detail/ui/DetailHeader/index.tsx
new file mode 100644
index 0000000..7dd7e54
--- /dev/null
+++ b/src/entities/expo-detail/ui/DetailHeader/index.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import React from 'react';
+import { ArrowLeft } from '@/shared/assets/icons';
+
+interface Props {
+ headerTitle: string;
+}
+
+const DetailHeader = ({ headerTitle }: Props) => {
+ const router = useRouter();
+
+ return (
+
+
+
{headerTitle}
+
+ );
+};
+
+export default DetailHeader;
diff --git a/src/entities/expo-detail/ui/KaKaoMap/index.tsx b/src/entities/expo-detail/ui/KaKaoMap/index.tsx
new file mode 100644
index 0000000..a349943
--- /dev/null
+++ b/src/entities/expo-detail/ui/KaKaoMap/index.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import Script from 'next/script';
+import { useKakaoMap } from '../../model/useKakaoMap';
+
+interface KakaoMapProps {
+ latitude: number;
+ longitude: number;
+}
+
+const KakaoMap = ({ latitude, longitude }: KakaoMapProps) => {
+ const { mapRef, loadKakaoMap } = useKakaoMap({ latitude, longitude });
+
+ return (
+ <>
+
+
+
+
+
+
${selectedData.name}
+
+ ${
+ isBase64
+ ? `

`
+ : `
`
+ }
+
+
+
+
+