diff --git a/.eslintrc.json b/.eslintrc.json index 24ad2aa..317c458 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,7 +31,7 @@ { "ts": "never", "tsx": "never" } ], "no-shadow": "off", - "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/no-shadow": "off", "@typescript-eslint/explicit-function-return-type": [ "error", { "allowExpressions": true } diff --git a/.github/workflows/discord-pr.yaml b/.github/workflows/discord-pr.yaml new file mode 100644 index 0000000..4b40806 --- /dev/null +++ b/.github/workflows/discord-pr.yaml @@ -0,0 +1,25 @@ +name: Discord PR Notification + +on: + pull_request: + types: [opened, reopened] + +jobs: + notify_discord: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Discord Notification + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_PR_WEBHOOK }} + DISCORD_USERNAME: GitHub + DISCORD_AVATAR: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + uses: Ilshidur/action-discord@master + with: + args: | + 새로운 PR이 열렸습니다!<@${{ secrets.DISCORD_ID_1 }}> <@${{ secrets.DISCORD_ID_2 }}> + PR: ${{ github.event.pull_request.html_url }} + 작성자: ${{ github.event.pull_request.user.login }} + 제목: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 582e3dd..60d28d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,18 +7,81 @@ jobs: build: runs-on: ubuntu-20.04 steps: - - name: Checkout source code. + - name: Checkout source code uses: actions/checkout@v3 - + with: + fetch-depth: 0 + + - name: Get commit message and author + id: get_commit_info + run: | + echo "::set-output name=message::$(git log --format=%s -n 1)" + echo "::set-output name=author::$(git log --format=%an -n 1)" + echo "::set-output name=author_username::$(git log --format=%ae -n 1 | cut -d@ -f1)" + - name: Install dependencies run: yarn install - + - name: Generate build + id: build + env: + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} + VITE_OAUTH_KAKAO_REST_API_KEY: ${{ secrets.VITE_OAUTH_KAKAO_REST_API_KEY }} + VITE_OAUTH_KAKAO_CLIENT_SECRET_CODE: ${{ secrets.VITE_OAUTH_KAKAO_CLIENT_SECRET_CODE }} + VITE_OAUTH_KAKAO_REDIRECT_URI: ${{ secrets.VITE_OAUTH_KAKAO_REDIRECT_URI }} run: yarn build - - - name: Deploy + continue-on-error: true + + - name: Deploy to S3 + id: deploy + if: steps.build.outcome == 'success' env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | aws s3 sync --region ap-northeast-2 dist s3://alignlab-client --delete + continue-on-error: true + + - name: Invalidate CloudFront Cache + if: steps.deploy.outcome == 'success' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} + run: | + aws cloudfront create-invalidation --region ap-northeast-2 --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" + + - name: Discord notification - Success + if: steps.deploy.outcome == 'success' + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEPLOY_WEBHOOK }} + DISCORD_USERNAME: GitHub + DISCORD_AVATAR: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + uses: Ilshidur/action-discord@master + with: + args: | + 🎉 배포가 성공적으로 완료되었습니다! + 브랜치: develop + 커밋: ${{ steps.get_commit_info.outputs.message }} + 작성자: ${{ steps.get_commit_info.outputs.author }} + + - name: Discord notification - Failure + if: steps.build.outcome == 'failure' || steps.deploy.outcome == 'failure' + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEPLOY_WEBHOOK }} + DISCORD_USERNAME: GitHub + DISCORD_AVATAR: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + uses: Ilshidur/action-discord@master + with: + args: | + ❌ ${{ steps.build.outcome == 'failure' && '빌드 중' || '배포 중' }} 오류가 발생했습니다. + 브랜치: develop + 커밋: ${{ steps.get_commit_info.outputs.message }} + 작성자: <@${{ secrets.DISCORD_ID_1 }}> + 실패한 워크플로우: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + ${{ steps.build.outcome == 'failure' && '빌드 오류 메시지:' || '' }} + ${{ steps.build.outcome == 'failure' && steps.build.outputs.stderr || '' }} + + - name: Check deploy result + if: steps.build.outcome == 'failure' || steps.deploy.outcome == 'failure' + run: exit 1 diff --git a/index.html b/index.html index d82565f..f353d30 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,9 @@ - - 자세 공작소 + + + 자세공작소
diff --git a/package.json b/package.json index cb71905..8a1d560 100644 --- a/package.json +++ b/package.json @@ -13,35 +13,48 @@ "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix" }, "dependencies": { + "@tanstack/react-query": "^5.51.23", + "@types/react-lottie": "^1.2.10", + "axios": "1.7.3", "core-js": "^3.28.0", + "dayjs": "^1.11.13", + "echarts": "^5.5.1", + "echarts-for-react": "^3.0.2", + "lucide-react": "^0.435.0", "p5": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1" + "react-lottie": "^1.2.4", + "react-router-dom": "^6.8.1", + "react-tailwindcss-datepicker": "^1.7.2", + "socket.io-client": "^4.7.5", + "zustand": "^4.5.5" }, "devDependencies": { - "@types/node": "^20.14.10", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "@typescript-eslint/eslint-plugin": "^5.54.1", - "@typescript-eslint/parser": "^5.54.1", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-config-prettier": "^8.7.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", + "@types/node": "^20.7.1", + "@types/qs": "^6.9.7", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.10", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.14", + "eslint": "^8.43.0", + "eslint-config-prettier": "^8.6.0", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.2", - "prettier": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.5", - "react-refresh": "^0.14.2", - "tailwindcss": "^3.4.4", + "eslint-plugin-react": "^7.32.1", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "prettier-plugin-tailwindcss": "^0.2.5", + "qs": "^6.11.0", + "react-refresh": "^0.14.0", + "tailwindcss": "^3.3.1", "ts-node": "^10.9.1", - "typescript": "^5.5.3", - "vite": "^5.3.3", + "typescript": "^5.1.6", + "vite": "^4.3.9", "vite-plugin-svgr": "^4.2.0", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^4.2.0" } } diff --git a/src/App.tsx b/src/App.tsx index 3089403..c7f0d58 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,18 @@ // dependencies -import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom" +import { Router } from "@/routes" -// components -import { PoseDetector } from "./components" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import ModalsProvider from "./providers/ModalsProvider" -const App: React.FC = () => { +const queryClient = new QueryClient() + +const App = (): React.ReactElement => { return ( - - - } /> - } /> - - + + + + + ) } diff --git a/src/api/analysis.ts b/src/api/analysis.ts new file mode 100644 index 0000000..fe4ace5 --- /dev/null +++ b/src/api/analysis.ts @@ -0,0 +1,42 @@ +import axiosInstance from "./axiosInstance" +import { poseType } from "./pose" + +export interface TodayAnalysisData { + date: string + count: { + type: poseType + count: number + }[] +} + +export interface TotalAnalysisData { + data: TodayAnalysisData[] + page: number + size: number + totalPage: number + totalCount: number + sort: { + empty: boolean + sorted: boolean + unsorted: boolean + } +} + +export const getTodayPoseAnalysis = async (): Promise => { + try { + const res = await axiosInstance.get("/pose-counts/daily") + return res.data.data + } catch (e) { + throw e + } +} + +export const getTotalPoseAnalysis = async (params: { fromDate?: string; toDate?: string }) => { + const queryString = new URLSearchParams(params).toString() + try { + const res = await axiosInstance.get(`/pose-counts?${queryString ? `${queryString}` : ""}&sort=date,asc`) + return res.data.data + } catch (e) { + throw e + } +} diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..1e5c781 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,87 @@ +// src/api/auth.ts +import axiosInstance, { setAccessToken } from "@/api/axiosInstance" +import qs from "qs" + +const REST_API_KEY = import.meta.env.VITE_OAUTH_KAKAO_REST_API_KEY +const CLIENT_SECRET = import.meta.env.VITE_OAUTH_KAKAO_CLIENT_SECRET_CODE +const REDIRECT_URI = import.meta.env.VITE_OAUTH_KAKAO_REDIRECT_URI + +export interface authUser { + uid: number + nickname: string + accessToken: string +} + +export interface oauthUser { + nickname: string +} + +export const oauth = async (code: string): Promise => { + const formData = { + grant_type: "authorization_code", + client_id: REST_API_KEY, + client_secret: CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code, + } + + try { + const res = await axiosInstance.post(`https://kauth.kakao.com/oauth/token?${qs.stringify(formData)}`, null, { + headers: { "Content-type": "application/x-www-form-urlencoded" }, + }) + return res.data.access_token + } catch (e) { + throw e + } +} + +export const getOauthUser = async (accessToken: string): Promise => { + const kakaoUser = await axiosInstance.get(`https://kapi.kakao.com/v2/user/me`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + const { nickname } = kakaoUser.data.kakao_account.profile + + return { nickname } +} + +export const signIn = async (_accessToken: string): Promise => { + try { + const res = await axiosInstance.post(`/oauth/kakao/sign-in`, { accessToken: _accessToken }) + const { uid, nickname, accessToken } = res.data.data + + // 로그인 성공 후 엑세스 토큰을 설정 + setAccessToken(accessToken) + + return { uid, nickname, accessToken } + } catch (e) { + throw e + } +} + +export const signUp = async (_accessToken: string): Promise => { + try { + const res = await axiosInstance.post(`/oauth/kakao/sign-up`, { accessToken: _accessToken }) + const { uid, nickname, accessToken } = res.data.data + + // 회원가입 성공 후 엑세스 토큰을 설정 + setAccessToken(accessToken) + + return { uid, nickname, accessToken } + } catch (e) { + throw e + } +} + +export const getIsSignUp = async (accessToken: string): Promise => { + try { + const res = await axiosInstance.get(`/oauth/kakao/sign-up/check`, { + params: { accessToken }, + }) + console.log(res.data) + return res.data.data.isExistsUser + } catch (e) { + throw e + } +} diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts new file mode 100644 index 0000000..7552d62 --- /dev/null +++ b/src/api/axiosInstance.ts @@ -0,0 +1,44 @@ +// src/services/axiosInstance.ts +import axios from "axios" + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL +const EXCEPT_HEADER_API = ["/token", "/user/me", "/oauth"] + +const axiosInstance = axios.create({ + baseURL: API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}) + +// 요청 인터셉터 설정 +axiosInstance.interceptors.request.use((config) => { + // 특정 API 경로에 대해 토큰을 제거 + if (config.url) { + // 요청 URL이 EXCEPT_HEADER_API에 포함되어 있는지 확인 + if (EXCEPT_HEADER_API.some((api) => config.url?.includes(api))) { + delete config.headers["X-HERO-AUTH-TOKEN"] + } + } + return config +}) + +// localStorage에서 토큰 가져오기 +const token = localStorage.getItem("accessToken") +if (token) { + axiosInstance.defaults.headers.common["X-HERO-AUTH-TOKEN"] = token +} + +// 엑세스 토큰 설정 함수 +export const setAccessToken = (_token: string): void => { + axiosInstance.defaults.headers.common["X-HERO-AUTH-TOKEN"] = _token + localStorage.setItem("accessToken", _token) +} + +// 엑세스 토큰 제거 함수 +export const clearAccessToken = (): void => { + delete axiosInstance.defaults.headers.common["X-HERO-AUTH-TOKEN"] + localStorage.removeItem("accessToken") +} + +export default axiosInstance diff --git a/src/api/group.ts b/src/api/group.ts new file mode 100644 index 0000000..b9bdd0c --- /dev/null +++ b/src/api/group.ts @@ -0,0 +1,160 @@ +import qs from "qs" +import axiosInstance from "./axiosInstance" +import { AxiosError } from "axios" + +export type sort = "userCount,desc" | "createdAt,desc" + +export interface sortRes { + empty: boolean + sorted: boolean + unsorted: boolean +} + +export interface group { + id?: number + name?: string + description?: string + ownerUid?: number + ownerName?: string + isHidden?: boolean + joinCode?: string + userCount?: number + userCapacity?: number + hasJoined?: boolean + ranks?: groupUserRank[] +} + +export interface groupUserRank { + groupUserId: number + name: string + rank: number + score: number +} + +export interface GroupUserRankData { + groupId: number + avgScore: number + ranks: groupUserRank[] +} + +export interface groupsReq { + page: number + size: number + sort: sort +} + +export interface groupsRes { + data: group[] + page: number + size: number + totalPage: number + totalCount: number + sort: sortRes +} + +export interface groupJoinReq { + groupId: number + joinCode?: string +} + +export interface groupJoinRes { + groupId: number + uid: number + groupUserId: number +} + +export interface MyGroupData { + id: string + name: string + description: string + userCount: number + userCapacity: number + ownerNickname: string +} + +export const getGroups = async (groupsReq: groupsReq): Promise => { + try { + const res = await axiosInstance.get(`/groups?${qs.stringify(groupsReq)}`) + return res.data + } catch (e) { + throw e + } +} + +export const getGroup = async (id: number | undefined): Promise => { + try { + const res = await axiosInstance.get(`/groups/${id}`) + return res.data.data + } catch (e) { + throw e + } +} + +export const joinGroup = async (groupJoinReq: groupJoinReq): Promise => { + try { + const res = await axiosInstance.post( + `groups/${groupJoinReq.groupId}/join`, + {}, // POST 요청에 body가 없다면 빈 객체 전달 + { + params: groupJoinReq.joinCode ? { joinCode: groupJoinReq.joinCode } : {}, // query string으로 joinCode 전달 + } + ) + return res.data.data + } catch (e) { + throw e + } +} + +export const checkGroupName = async (name: string): Promise => { + try { + // eslint-disable-next-line max-len + const res = await axiosInstance.post(`groups/check`, { name }) + const errorCode = res.data?.errorCode + return !errorCode + } catch (e) { + const { response } = e as AxiosError + const data = response?.data as { errorCode: string; reason: string } + if (data.errorCode) return false + throw e + } +} + +export const createGroup = async (group: group): Promise => { + try { + const res = await axiosInstance.post(`groups`, { ...group }) + return res.data.data + } catch (e) { + throw e + } +} + +export const getGroupScores = async (groupdId: string | number): Promise<{ data: GroupUserRankData }> => { + try { + const res = await axiosInstance.get(`/group-scores?groupId=${groupdId}`) + return res.data + } catch (e) { + throw e + } +} + +export const getMyGroup = async (): Promise<{ data: MyGroupData }> => { + try { + const res = await axiosInstance.get(`/groups/my-group`) + return res.data + } catch (e) { + throw e + } +} + +export const withdrawMyGroup = async (id: string | number | undefined): Promise => { + if (!id) { + throw new Error("잘못된 크루 id 입니다.") + } + try { + const res = await axiosInstance.delete(`/groups/${id}/withdraw`) + console.log("res: ", res) + return res + } catch (e) { + throw e + } +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..aae0a28 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,4 @@ +export * from "./auth" +export * from "./snapshot" +export * from "./group" +export * from "./report" diff --git a/src/api/notification.ts b/src/api/notification.ts new file mode 100644 index 0000000..d1cd322 --- /dev/null +++ b/src/api/notification.ts @@ -0,0 +1,37 @@ +import axiosInstance from "./axiosInstance" + +export type duration = "IMMEDIATELY" | "MIN_15" | "MIN_30" | "MIN_45" | "MIN_60" + +export interface notification { + id?: number + isActive?: boolean + duration?: duration +} + +export const getNotification = async (): Promise<{ data: notification }> => { + try { + const res = await axiosInstance.get(`/pose-notifications`) + return res.data + } catch (e) { + throw e + } +} + +export const registerNotification = async (notification: notification): Promise => { + try { + const res = await axiosInstance.post(`/pose-notifications`, { ...notification }) + return res.data.data + } catch (e) { + throw e + } +} + +// export const updateNotification = async (notification: notification): Promise => { +// try { +// const res = await axiosInstance.patch(`/pose-notifications/${notification.id}`, { ...notification }) +// const { id, isActive, duration } = res.data.data +// return { id, isActive, duration } +// } catch (e) { +// throw e +// } +// } diff --git a/src/api/pose.ts b/src/api/pose.ts new file mode 100644 index 0000000..e1b6fed --- /dev/null +++ b/src/api/pose.ts @@ -0,0 +1,26 @@ +import { pose } from "@/utils" +import axiosInstance from "./axiosInstance" + +export type poseType = "GOOD" | "TURTLE_NECK" | "SHOULDER_TWIST" | "CHIN_UTP" | "TAILBONE_SIT" + +export interface poseReq { + snapshot: pose + type: poseType + imageUrl?: string +} + +export interface poseRes { + id: number + uid: number + type: poseType + createdAt: string +} +export const sendPose = async (poseReq: poseReq): Promise => { + try { + const res = await axiosInstance.post(`/pose-snapshots`, { ...poseReq }) + const { id, uid, type, createdAt } = res.data.data + return { id, uid, type, createdAt } + } catch (e) { + throw e + } +} diff --git a/src/api/report.ts b/src/api/report.ts new file mode 100644 index 0000000..e2d2219 --- /dev/null +++ b/src/api/report.ts @@ -0,0 +1,29 @@ +import axiosInstance from "./axiosInstance" + +interface ReportResData { + data: { + id: number + uid: number + email: string + type: string + title: string + content: string + createdAt: string + modifiedAt: string + } +} + +interface ReportParam { + email: string + title: string + content: string +} + +export const requestSendReportAPI = async (param: ReportParam): Promise => { + try { + const res = await axiosInstance.post(`/discussion`, { ...param, type: "QNA" }) + return res.data + } catch (e) { + throw e + } +} diff --git a/src/api/snapshot.ts b/src/api/snapshot.ts new file mode 100644 index 0000000..de3ae27 --- /dev/null +++ b/src/api/snapshot.ts @@ -0,0 +1,64 @@ +import axiosInstance from "./axiosInstance" + +export type position = + | "NOSE" + | "LEFT_EYE" + | "RIGHT_EYE" + | "LEFT_EAR" + | "RIGHT_EAR" + | "LEFT_SHOULDER" + | "RIGHT_SHOULDER" + | "LEFT_ELBOW" + | "RIGHT_ELBOW" + | "LEFT_WRIST" + | "RIGHT_WRIST" + | "LEFT_HIP" + | "RIGHT_HIP" + | "LEFT_KNEE" + | "RIGHT_KNEE" + | "LEFT_ANKLE" + | "RIGHT_ANKLE" + +export interface point { + position: position + x: number + y: number +} + +export interface snapshot { + id?: number + points: point[] +} + +export interface createSnapshotRes { + id: string +} + +export const createSnapshot = async (snapshot: snapshot): Promise => { + try { + const res = await axiosInstance.post(`/pose-layouts`, { points: snapshot.points }) + const { id } = res.data.data + + return { id } + } catch (e) { + throw e + } +} + +export const getSnapshots = async (id: string): Promise => { + try { + const res = await axiosInstance.get(`/pose-layouts/${id}`) + return res.data.data + } catch (e) { + throw e + } +} + +export const getRecentSnapshot = async (): Promise => { + try { + const res = await axiosInstance.get(`/pose-layouts/recent`) + return res.data.data + } catch (e) { + throw e + } +} diff --git a/src/assets/animation/check-lottie.json b/src/assets/animation/check-lottie.json new file mode 100644 index 0000000..f8229c4 --- /dev/null +++ b/src/assets/animation/check-lottie.json @@ -0,0 +1 @@ +{"v":"5.7.12","fr":24,"ip":0,"op":63,"w":520,"h":520,"nm":"Checklist 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Checklist","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[100]},{"t":62,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[259.587,260.119,0],"ix":2,"l":2},"a":{"a":0,"k":[297.587,298.119,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[54.754,-36.121],[-17.487,36.12],[-54.754,-1.147]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":23,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[294.971,298.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[100]},{"t":36,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Cricle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[100]},{"t":62,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[-7.627,-7.691,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":18,"s":[124.222,124.222,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[134.222,134.222,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":22,"s":[114.222,114.222,100]},{"t":24,"s":[124.222,124.222,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[178.46,178.46],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.239],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":24,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.074509803922,0.596078431373,0.349019607843,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.074509803922,0.596078431373,0.349019607843,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"t":24,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-7.627,-7.691],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":50,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[298,298,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":28,"s":[46,46,100]},{"t":50,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-120.208,0],[0,-120.208],[120.207,0],[0,120.208]],"o":[[120.207,0],[0,120.208],[-120.208,0],[0,-120.208]],"v":[[0,-217.655],[217.655,0],[0,217.655],[-217.655,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.65023354923,0.829386991613,0.709956449621,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[297.587,298.119],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":720,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shadow","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":33,"s":[100]},{"t":44,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[298,298,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":22,"s":[60,60,100]},{"t":44,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-94.797,0],[0,-94.797],[94.797,0],[0,94.798]],"o":[[94.797,0],[0,94.798],[-94.797,0],[0,-94.797]],"v":[[0,-171.646],[171.646,0],[0,171.646],[-171.646,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.888151161343,0.944075939702,0.895734480316,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[297.587,298.119],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":720,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/src/assets/animation/login-lottie.json b/src/assets/animation/login-lottie.json new file mode 100644 index 0000000..92867ec --- /dev/null +++ b/src/assets/animation/login-lottie.json @@ -0,0 +1 @@ +{"v":"5.1.15","fr":60,"ip":0,"op":141,"w":300,"h":300,"nm":"Composição 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Camada de forma 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1080]},{"t":180}],"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[-14.604,-15.104,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.793,106.793],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.0941,0.0941,0.1059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":11,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-14.604,-15.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar","_render":true}],"nm":"Elipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[0],"e":[67]},{"t":119}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[3],"e":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[70],"e":[70]},{"t":119}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true}],"ip":0,"op":900,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":2,"ty":4,"nm":"Camada de forma 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1080]},{"t":180}],"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[-14.604,-15.104,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.793,106.793],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0,0,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-14.604,-15.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar","_render":true}],"nm":"Elipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":20,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":69.916,"s":[0],"e":[67]},{"t":119}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":20,"s":[3],"e":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":69.916,"s":[70],"e":[70]},{"t":119}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true}],"ip":0,"op":900,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":3,"ty":4,"nm":"Camada de forma 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1080]},{"t":180}],"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[-14.604,-15.104,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.793,106.793],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0,0,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-14.604,-15.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar","_render":true}],"nm":"Elipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":41,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":80.328,"s":[0],"e":[67]},{"t":119}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":41,"s":[3],"e":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":80.328,"s":[70],"e":[70]},{"t":119}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true}],"ip":0,"op":900,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":4,"ty":4,"nm":"Camada de forma 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1080]},{"t":180}],"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[-14.604,-15.104,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.793,106.793],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0,0,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-14.604,-15.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar","_render":true}],"nm":"Elipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":54,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":86.773,"s":[0],"e":[67]},{"t":119}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":54,"s":[3],"e":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":86.773,"s":[70],"e":[70]},{"t":119}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false,"_render":true}],"ip":0,"op":900,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true} \ No newline at end of file diff --git a/src/assets/animation/script-loading-lottie.json b/src/assets/animation/script-loading-lottie.json new file mode 100644 index 0000000..58bb7bb --- /dev/null +++ b/src/assets/animation/script-loading-lottie.json @@ -0,0 +1 @@ +{"nm":"Camera","mn":"","layers":[{"ty":4,"nm":"Sensor Outlines","mn":"","sr":1,"st":34,"op":60,"ip":36,"hd":false,"cl":"","ln":"","ddd":0,"bm":0,"tt":0,"hasMask":false,"td":0,"ao":0,"ks":{"a":{"a":0,"k":[16.833,16.819,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.066,"y":-0.007},"i":{"x":0.595,"y":1.797},"s":[0,0,100],"t":36},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[50,50,100],"t":44}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[361.301,224.644,0],"ix":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10}},"ef":[],"shapes":[{"ty":"gr","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0.067,-9.428],[9.387,0.072],[-0.09,9.304],[-9.375,-0.073]],"o":[[-0.066,9.383],[-9.314,-0.071],[0.09,-9.336],[9.391,0.072]],"v":[[16.516,0.131],[-0.101,16.498],[-16.493,-0.162],[0.197,-16.497]]},"ix":2}},{"ty":"fl","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[16.832,16.82],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":0},{"ty":4,"nm":"Lens 3","mn":"","sr":1,"st":5,"op":60,"ip":5,"hd":false,"cl":"","ln":"","ddd":0,"bm":0,"tt":0,"hasMask":false,"td":0,"ao":0,"ks":{"a":{"a":0,"k":[-0.494,27.5,0],"ix":1},"s":{"a":0,"k":[60,60,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256.006,270.25,0],"ix":2},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":23},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":24},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":25}],"ix":11},"r":{"a":0,"k":60,"ix":10}},"ef":[],"shapes":[{"ty":"gr","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[191,191],"ix":2}},{"ty":"st","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"d":[],"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-0.5,27.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":1,"y":0.543},"i":{"x":0.03,"y":0.474},"s":[0],"t":8},{"o":{"x":1,"y":8.35},"i":{"x":0.667,"y":1},"s":[100],"t":21},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":23},{"o":{"x":0.333,"y":0},"i":{"x":0.86,"y":-7.77},"s":[0],"t":25},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":40}],"ix":2},"o":{"a":0,"k":-60,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":0.446},"s":[0],"t":9},{"o":{"x":0.333,"y":-0.08},"i":{"x":0,"y":1},"s":[97],"t":23},{"o":{"x":1,"y":0},"i":{"x":0,"y":1},"s":[1],"t":25},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":40}],"ix":1},"m":1}],"ind":1},{"ty":4,"nm":"Body Outlines 2","mn":"","sr":1,"st":0,"op":60,"ip":0,"hd":false,"cl":"","ln":"","ddd":0,"bm":0,"tt":0,"hasMask":false,"td":0,"ao":0,"ks":{"a":{"a":0,"k":[240.356,192.29,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":0.553,"y":1},"s":[60,0,100],"t":1},{"o":{"x":0.542,"y":0},"i":{"x":0.997,"y":0.878},"s":[60,62.568,100],"t":15},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[60,60,100],"t":25}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256.023,256.008,0],"ix":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10}},"ef":[],"shapes":[{"ty":"gr","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-61.86,0.003],[-62.485,0.103],[-0.015,32.529],[0.121,69.983],[28.775,0.198],[17.182,0.05],[4.261,12.509],[4.38,12.672],[11.7,0.028],[52.8,-0.124],[3.856,-11.192],[4.454,-12.976],[12.153,-0.062],[16.559,-0.026],[0.015,-32.478],[-0.011,-68.421],[-35.292,-0.024]],"o":[[62.485,0],[32.82,-0.055],[0.035,-69.983],[-0.05,-28.733],[-17.182,-0.118],[-13.417,-0.039],[-4.326,-12.691],[-3.88,-11.229],[-52.799,-0.127],[-11.72,0.028],[-4.469,12.971],[-3.894,11.346],[-16.558,0.086],[-32.598,0.05],[-0.033,68.421],[0.006,35.149],[61.86,0.044]],"v":[[-0.058,191.967],[187.397,191.937],[240.015,139.573],[239.985,-70.376],[190.26,-120.028],[138.711,-120.064],[114.291,-137.407],[101.424,-175.517],[79.2,-191.913],[-79.199,-191.915],[-101.463,-175.572],[-114.572,-136.557],[-137.771,-120.075],[-187.447,-120.047],[-240.073,-67.623],[-240.08,137.64],[-185.638,191.957]]},"ix":2}},{"ty":"fl","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.2,0.4235,0.9765],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[240.356,192.29],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Burst","mn":"","sr":1,"st":32,"op":60,"ip":32,"hd":false,"cl":"","ln":"","ddd":0,"bm":0,"tt":0,"hasMask":false,"td":0,"ao":0,"ks":{"a":{"a":0,"k":[0,116,0],"ix":1},"s":{"a":0,"k":[113,113,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256,257.08,0],"ix":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10}},"ef":[],"shapes":[{"ty":"gr","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.25],[0,-88]]},"ix":2}},{"ty":"st","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"d":[],"c":{"a":0,"k":[0.2,0.4235,0.9765],"ix":3}},{"ty":"rp","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Filter - Repeater","nm":"Repeater 1","ix":4,"m":1,"c":{"a":0,"k":10,"ix":1},"o":{"a":0,"k":0,"ix":2},"tr":{"a":{"a":0,"k":[0,116],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":36,"ix":4},"sa":{"a":0,"k":0},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6}}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"cl":"","ln":"","hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":43},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":49}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":45},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":57}],"ix":1},"m":1}],"ind":3}],"ddd":0,"h":512,"w":512,"meta":{"a":"","k":"","d":"","g":"@lottiefiles/toolkit-js 0.17.3","tc":"#ffffff"},"v":"5.5.8","fr":30,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/assets/icons/crew-1st-crown.svg b/src/assets/icons/crew-1st-crown.svg new file mode 100644 index 0000000..2c021ae --- /dev/null +++ b/src/assets/icons/crew-1st-crown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-checked-icon.svg b/src/assets/icons/crew-checked-icon.svg new file mode 100644 index 0000000..45d8244 --- /dev/null +++ b/src/assets/icons/crew-checked-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/crew-create-button-icon.svg b/src/assets/icons/crew-create-button-icon.svg new file mode 100644 index 0000000..100a930 --- /dev/null +++ b/src/assets/icons/crew-create-button-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/crew-join-user-icon.svg b/src/assets/icons/crew-join-user-icon.svg new file mode 100644 index 0000000..f557696 --- /dev/null +++ b/src/assets/icons/crew-join-user-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/crew-my-crew-header-flag.svg b/src/assets/icons/crew-my-crew-header-flag.svg new file mode 100644 index 0000000..0bb1617 --- /dev/null +++ b/src/assets/icons/crew-my-crew-header-flag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-my-crew-leader-icon.svg b/src/assets/icons/crew-my-crew-leader-icon.svg new file mode 100644 index 0000000..a52a7f1 --- /dev/null +++ b/src/assets/icons/crew-my-crew-leader-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/icons/crew-panel-close-button.svg b/src/assets/icons/crew-panel-close-button.svg new file mode 100644 index 0000000..ce7b446 --- /dev/null +++ b/src/assets/icons/crew-panel-close-button.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/crew-private-icon.svg b/src/assets/icons/crew-private-icon.svg new file mode 100644 index 0000000..c3f34e8 --- /dev/null +++ b/src/assets/icons/crew-private-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/crew-send-invitation.svg b/src/assets/icons/crew-send-invitation.svg new file mode 100644 index 0000000..5992248 --- /dev/null +++ b/src/assets/icons/crew-send-invitation.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-side-nav-down-arrow.svg b/src/assets/icons/crew-side-nav-down-arrow.svg new file mode 100644 index 0000000..07c2ed7 --- /dev/null +++ b/src/assets/icons/crew-side-nav-down-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-sort-icon.svg b/src/assets/icons/crew-sort-icon.svg new file mode 100644 index 0000000..ae8c5c3 --- /dev/null +++ b/src/assets/icons/crew-sort-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-unckecked-icon.svg b/src/assets/icons/crew-unckecked-icon.svg new file mode 100644 index 0000000..6ebc409 --- /dev/null +++ b/src/assets/icons/crew-unckecked-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-user-icon.svg b/src/assets/icons/crew-user-icon.svg new file mode 100644 index 0000000..8bb83dd --- /dev/null +++ b/src/assets/icons/crew-user-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/dash-board-total-count.svg b/src/assets/icons/dash-board-total-count.svg new file mode 100644 index 0000000..78720c3 --- /dev/null +++ b/src/assets/icons/dash-board-total-count.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/dashboard-calendar-tooltip.svg b/src/assets/icons/dashboard-calendar-tooltip.svg new file mode 100644 index 0000000..12ec27d --- /dev/null +++ b/src/assets/icons/dashboard-calendar-tooltip.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/favicon.svg b/src/assets/icons/favicon.svg new file mode 100644 index 0000000..4d6782f --- /dev/null +++ b/src/assets/icons/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/good-posture-check-button-icon.svg b/src/assets/icons/good-posture-check-button-icon.svg new file mode 100644 index 0000000..5f7c4a7 --- /dev/null +++ b/src/assets/icons/good-posture-check-button-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/group-side-nav-button.svg b/src/assets/icons/group-side-nav-button.svg new file mode 100644 index 0000000..7769f95 --- /dev/null +++ b/src/assets/icons/group-side-nav-button.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/home-kakao-signup-button-icon.svg b/src/assets/icons/home-kakao-signup-button-icon.svg new file mode 100644 index 0000000..e62f58c --- /dev/null +++ b/src/assets/icons/home-kakao-signup-button-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/home-logo.svg b/src/assets/icons/home-logo.svg new file mode 100644 index 0000000..b012c67 --- /dev/null +++ b/src/assets/icons/home-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/modal-close-icon.svg b/src/assets/icons/modal-close-icon.svg new file mode 100644 index 0000000..6551b5e --- /dev/null +++ b/src/assets/icons/modal-close-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/posture-craft-side-nav-icon.svg b/src/assets/icons/posture-craft-side-nav-icon.svg new file mode 100644 index 0000000..83fd5a5 --- /dev/null +++ b/src/assets/icons/posture-craft-side-nav-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/icons/posture-guide-button-icon.svg b/src/assets/icons/posture-guide-button-icon.svg new file mode 100644 index 0000000..326e45b --- /dev/null +++ b/src/assets/icons/posture-guide-button-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/posture-snapshot-retake-icon.svg b/src/assets/icons/posture-snapshot-retake-icon.svg new file mode 100644 index 0000000..a643b44 --- /dev/null +++ b/src/assets/icons/posture-snapshot-retake-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/question-info-icon.svg b/src/assets/icons/question-info-icon.svg new file mode 100644 index 0000000..6cf024e --- /dev/null +++ b/src/assets/icons/question-info-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/side-nav-analysis-icon.svg b/src/assets/icons/side-nav-analysis-icon.svg new file mode 100644 index 0000000..9aea821 --- /dev/null +++ b/src/assets/icons/side-nav-analysis-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/side-nav-crew-icon.svg b/src/assets/icons/side-nav-crew-icon.svg new file mode 100644 index 0000000..681225a --- /dev/null +++ b/src/assets/icons/side-nav-crew-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/side-nav-logo.svg b/src/assets/icons/side-nav-logo.svg new file mode 100644 index 0000000..980c5b6 --- /dev/null +++ b/src/assets/icons/side-nav-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/icons/side-nav-monitor-icon.svg b/src/assets/icons/side-nav-monitor-icon.svg new file mode 100644 index 0000000..9ea3fc7 --- /dev/null +++ b/src/assets/icons/side-nav-monitor-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/chin-up.png b/src/assets/images/chin-up.png new file mode 100644 index 0000000..e8dbbc4 Binary files /dev/null and b/src/assets/images/chin-up.png differ diff --git a/src/assets/images/crew-empty.png b/src/assets/images/crew-empty.png new file mode 100644 index 0000000..a0b1e3f Binary files /dev/null and b/src/assets/images/crew-empty.png differ diff --git a/src/assets/images/home-intro.png b/src/assets/images/home-intro.png new file mode 100644 index 0000000..da7f4e8 Binary files /dev/null and b/src/assets/images/home-intro.png differ diff --git a/src/assets/images/home-monitoring.png b/src/assets/images/home-monitoring.png new file mode 100644 index 0000000..a35e320 Binary files /dev/null and b/src/assets/images/home-monitoring.png differ diff --git a/src/assets/images/mycrew-no-ranks.png b/src/assets/images/mycrew-no-ranks.png new file mode 100644 index 0000000..df07e62 Binary files /dev/null and b/src/assets/images/mycrew-no-ranks.png differ diff --git a/src/assets/images/posture-guide-2x.png b/src/assets/images/posture-guide-2x.png new file mode 100644 index 0000000..b9365a6 Binary files /dev/null and b/src/assets/images/posture-guide-2x.png differ diff --git a/src/assets/images/posture-snapshot-guide.png b/src/assets/images/posture-snapshot-guide.png new file mode 100644 index 0000000..cefc665 Binary files /dev/null and b/src/assets/images/posture-snapshot-guide.png differ diff --git a/src/assets/images/ranking-guide.png b/src/assets/images/ranking-guide.png new file mode 100644 index 0000000..d401373 Binary files /dev/null and b/src/assets/images/ranking-guide.png differ diff --git a/src/assets/images/shoulder-twist.png b/src/assets/images/shoulder-twist.png new file mode 100644 index 0000000..3fa86d4 Binary files /dev/null and b/src/assets/images/shoulder-twist.png differ diff --git a/src/assets/images/tail-bone-sit.png b/src/assets/images/tail-bone-sit.png new file mode 100644 index 0000000..09cce05 Binary files /dev/null and b/src/assets/images/tail-bone-sit.png differ diff --git a/src/assets/images/tutle-neck.png b/src/assets/images/tutle-neck.png new file mode 100644 index 0000000..4ae479d Binary files /dev/null and b/src/assets/images/tutle-neck.png differ diff --git a/src/components/Camera.tsx b/src/components/Camera.tsx index fd6d99f..090a967 100644 --- a/src/components/Camera.tsx +++ b/src/components/Camera.tsx @@ -1,20 +1,26 @@ import React, { useRef, useEffect } from "react" +import { useCameraPermission } from "@/hooks/useCameraPermission" // 커스텀 훅을 가져옵니다. -type WebcamProps = { - onStreamReady: (video: HTMLVideoElement) => void +interface CameraProps { + detectStart: (video: HTMLVideoElement) => void + canvasRef: React.LegacyRef | undefined } -const Camera: React.FC = ({ onStreamReady }) => { +export default function Camera(props: CameraProps): React.ReactElement { + const { detectStart, canvasRef } = props const videoRef = useRef(null) + // 비디오를 시작하는 함수 const startVideo = (): void => { navigator.mediaDevices .getUserMedia({ video: { facingMode: "user", frameRate: { - ideal: 5, + ideal: 60, }, + width: 1280, + height: 720, }, }) .then((stream) => { @@ -24,7 +30,7 @@ const Camera: React.FC = ({ onStreamReady }) => { videoRef.current.onloadedmetadata = () => { if (videoRef.current) { videoRef.current.play() - onStreamReady(videoRef.current) + detectStart(videoRef.current) } } } @@ -34,24 +40,78 @@ const Camera: React.FC = ({ onStreamReady }) => { }) } + // 비디오를 중지하는 함수 + const stopVideo = (): void => { + if (videoRef.current && videoRef.current.srcObject) { + const stream = videoRef.current.srcObject as MediaStream + const tracks = stream.getTracks() + + tracks.forEach((track) => { + track.stop() // 모든 트랙 중지 + }) + + videoRef.current.srcObject = null // 비디오 스트림 초기화 + } + } + + // 커스텀 훅을 사용해 권한 상태 확인 + const { hasPermission, isPermissionDenied } = useCameraPermission() + + useEffect(() => { + if (hasPermission) { + startVideo() + } else if (isPermissionDenied) { + stopVideo() + } + + return () => { + stopVideo() // 컴포넌트가 언마운트될 때 비디오 중지 + } + }, [hasPermission, isPermissionDenied]) + useEffect(() => { startVideo() + return () => { + stopVideo() // 컴포넌트가 언마운트될 때 비디오 중지 + } }, []) return ( -
-
) } - -export default Camera diff --git a/src/components/Crew/CrewItem.tsx b/src/components/Crew/CrewItem.tsx new file mode 100644 index 0000000..f5539c5 --- /dev/null +++ b/src/components/Crew/CrewItem.tsx @@ -0,0 +1,46 @@ +import { group } from "@/api" +import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react" +import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react" +import { ReactElement } from "react" + +interface CrewItemProps { + group: group + onClickDetail: () => void +} + +const CrewItem = (props: CrewItemProps): ReactElement => { + const { group, onClickDetail } = props + + return ( +
+ {/* crew name */} +
+ {group.isHidden && } +
{group.name}
+
+ {/* crew user cnt */} +
+ +
{`${group.userCount}/${group.userCapacity}명`}
+
+ {/* detail button */} + +
+ ) +} + +export default CrewItem diff --git a/src/components/Crew/CrewList.tsx b/src/components/Crew/CrewList.tsx new file mode 100644 index 0000000..ade6972 --- /dev/null +++ b/src/components/Crew/CrewList.tsx @@ -0,0 +1,218 @@ +import { group, groupsReq, sort } from "@/api" +import EmptyCrewImage from "@/assets/images/crew-empty.png" +import CrewItem from "@/components/Crew/CrewItem" +import { useGetGroups } from "@/hooks/useGroupMutation" +import { useModals } from "@/hooks/useModals" +import useMyGroup from "@/hooks/useMyGroup" +import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" +import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react" +import { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from "react" +import { modals } from "../Modal/Modals" +import MyCrewRankingContainer from "./MyCrew/MyCrewRankingContainer" +import { useNavigate, useSearchParams } from "react-router-dom" +import RoutePath from "@/constants/routes.json" + +const SORT_LIST = [ + { sort: "userCount,desc", label: "크루원 많은 순" }, + { sort: "createdAt,desc", label: "최신 생성 크루 순" }, +] + +const CrewList = (): ReactElement => { + const navigate = useNavigate() + const { myGroupData, ranks, myRank, refetchAll, isLoading: isGroupLoading } = useMyGroup() + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + // Dropdown 외부 클릭 감지 메모이제이션 + const dropdownRef = useRef(null) + const [searchParams, setSearchParams] = useSearchParams() + + const [params, setParams] = useState({ + page: 0, + size: 10000, + sort: "userCount,desc", + }) + + const { data, isLoading, isError, refetch } = useGetGroups(params) + const { openModal } = useModals() + + // 가입 혹은 그룹 생성 후 최상단으로 이동 + const scrollToTop = useCallback((): void => { + const el = document.getElementById("main-content") + if (el) { + el.scrollTo({ top: 0, behavior: "smooth" }) + } + }, []) + + // openCreateModal 메모이제이션 + const openCreateModal = useCallback((): void => { + if (myGroupData) { + openModal(modals.ToWithdrawModal, { + onSubmit: () => { + navigate(RoutePath.MYCREW) + }, + }) + } else { + openModal(modals.createCrewModal, { + onSubmit: () => { + refetch() + refetchAll() + scrollToTop() + }, + }) + } + }, [myGroupData, navigate, openModal, refetch, refetchAll]) + + // openJoinCrewModal 메모이제이션 + const openJoinCrewModal = useCallback( + (id: number | undefined): void => { + openModal(modals.joinCrewModal, { + id, + onSubmit: () => { + refetch() + refetchAll() + scrollToTop() + }, + }) + }, + [openModal] + ) + + // openInviteModal 메모이제이션 + const openInviteModal = useCallback((): void => { + openModal(modals.inviteCrewModal, { + id: Number(myGroupData?.id), + }) + }, [myGroupData, openModal]) + + // toggleDropdown 함수 + const toggleDropdown = (): void => { + setIsDropdownOpen((prev) => !prev) + } + + // createSortList 메모이제이션 + const createSortList = useMemo(() => { + return SORT_LIST.map((s) => ( +
{ + setParams((prev) => ({ ...prev, sort: s.sort as sort })) + setIsDropdownOpen(false) + }} + > + {s.label} +
+ )) + }, []) + + // createGroupList 메모이제이션 + const createGroupList = useCallback( + (_groups: group[] | undefined): JSX.Element | null => { + if (!_groups) return null + + if (_groups.length === 0) { + return ( +
+ empty crew +
+ {"만들어진 크루가 아직 없습니다."} +
+
+ ) + } + + return ( +
+ {_groups.map((g) => ( + openJoinCrewModal(g.id)} /> + ))} +
+ ) + }, + [openJoinCrewModal] + ) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as HTMLElement)) { + setIsDropdownOpen(false) + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, []) + + // URL에서 groupId 추출 후 모달 열기 + useEffect(() => { + const groupId = searchParams.get("groupId") + + if (groupId) { + openJoinCrewModal(Number(groupId)) + const removeGroupIdFromUrl = (): void => { + searchParams.delete("groupId") + setSearchParams(searchParams) + } + removeGroupIdFromUrl() + } + }, [openJoinCrewModal, searchParams, setSearchParams]) + + console.log("ranks: ", ranks) + + return ( +
+ {myGroupData && Object.keys(myGroupData).length > 0 && ( + + )} + + {/* header */} +
+
+ 전체크루 + {isLoading ? "" : `(${data?.totalCount})`} +
+ {(!myGroupData || (myGroupData && Object.keys(myGroupData).length === 0)) && ( +
+ +
크루 만들기
+
+ )} +
+ + {/* sort */} +
+
+ +
{SORT_LIST.find((s) => s.sort === params.sort)?.label}
+
+ + {/* dropdown */} +
+ {createSortList} +
+
+ + {/* list */} + {isLoading ? "로딩 중입니다..." : isError ? "데이터를 불러오는데 실패했습니다." : createGroupList(data?.data)} +
+ ) +} + +export default CrewList diff --git a/src/components/Crew/CrewRanking.tsx b/src/components/Crew/CrewRanking.tsx new file mode 100644 index 0000000..0240565 --- /dev/null +++ b/src/components/Crew/CrewRanking.tsx @@ -0,0 +1,96 @@ +import Crew1stCrownIcon from "@assets/icons/crew-1st-crown.svg?react" + +const RankPillar = ({ rank, name, score, height }: any) => { + const rankStyleMap: { gap: number | string; bgColor: string; fontSize: string; fontWeight: string }[] = [ + { + gap: 24, + bgColor: "#8BBAFE", + fontSize: "32px", + fontWeight: "600", + }, + { + gap: 24, + bgColor: "#DCEBFD", + fontSize: "22px", + fontWeight: "500", + }, + { + gap: 16, + bgColor: "#DCEBFD", + fontSize: "22px", + fontWeight: "500", + }, + ] + + const style = rankStyleMap[rank - 1] + + return ( +
+
+ {rank === 1 && } +
+ {rank}등 +
+
+
{name}
+
자세 경고 {score}회
+
+ ) +} + +const RankCard = ({ rank, name, score, isMe }: any) => ( +
+
+
{rank}
+ {isMe ? "나" : name} +
+ 자세경고 {score}회 +
+) + +const CrewRanking = ({ rankings, myRank }: { rankings: any[]; myRank: any }) => { + const topThree = rankings.slice(0, 3) + + return ( +
+ {/* 1, 2, 3등 랭킹 */} +
+ {topThree[0] && } + {topThree[1] && } + {topThree[2] && } +
+ + {/* 전체 랭킹 목록 */} +
+
+
+ +
+
+ {rankings.map((rank, index) => ( + + ))} +
+
+
+
+ ) +} + +export default CrewRanking diff --git a/src/components/Crew/MyCrew/MyCrewHeader.tsx b/src/components/Crew/MyCrew/MyCrewHeader.tsx new file mode 100644 index 0000000..6e67d30 --- /dev/null +++ b/src/components/Crew/MyCrew/MyCrewHeader.tsx @@ -0,0 +1,24 @@ +import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" + +interface MyCrewHeaderProps { + openCreateModal?: () => void + isDisplayedCreationButton?: boolean +} + +export default function MyCrewHeader(props: MyCrewHeaderProps) { + const { openCreateModal, isDisplayedCreationButton = false } = props + return ( +
+
나의 크루
+ {isDisplayedCreationButton && ( +
+ +
크루 만들기
+
+ )} +
+ ) +} diff --git a/src/components/Crew/MyCrew/MyCrewRankingContainer.tsx b/src/components/Crew/MyCrew/MyCrewRankingContainer.tsx new file mode 100644 index 0000000..db42fe3 --- /dev/null +++ b/src/components/Crew/MyCrew/MyCrewRankingContainer.tsx @@ -0,0 +1,80 @@ +import SendInvitationIcon from "@assets/icons/crew-send-invitation.svg?react" +import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react" +import { Link } from "react-router-dom" +import CrewRanking from "../CrewRanking" + +import RoutePath from "@/constants/routes.json" +import MyCrewHeader from "./MyCrewHeader" +import { groupUserRank, MyGroupData } from "@/api" +import dayjs from "dayjs" +import NoRanksImage from "@/assets/images/mycrew-no-ranks.png" + +interface MyCrewRankingContainerProps { + isLoading: boolean + myGroupData: MyGroupData + ranks: groupUserRank[] + myRank: groupUserRank | undefined + openCreateModal: () => void + openInviteModal: () => void +} + +export default function MyCrewRankingContainer(props: MyCrewRankingContainerProps) { + const { isLoading, myGroupData, ranks, myRank, openCreateModal, openInviteModal } = props + return ( +
+ +
+
+
+
{myGroupData.name}
+
+ + + {myGroupData.userCount}/{myGroupData.userCapacity}명 + +
+ +
+ +
상세보기 {">"}
+ +
+ {/* 랭킹 헤더 */} +
+
+ 바른자세 랭킹 +
+ 최근 1시간 + | + {dayjs().format("YYYY.MM.DD HH:mm")} 기준 +
+
+
+ + {/* 랭킹 표시 */} +
+ {ranks.length > 0 && myRank ? ( + + ) : ( + !isLoading && ( +
+
+ +
표시할 랭킹이 없습니다.
+
+
+ ) + )} +
+
+
+ ) +} diff --git a/src/components/Dashboard/Chart.tsx b/src/components/Dashboard/Chart.tsx new file mode 100644 index 0000000..193c523 --- /dev/null +++ b/src/components/Dashboard/Chart.tsx @@ -0,0 +1,53 @@ +import ECharts from "echarts-for-react" + +const poseTypeLabels: any = { + TURTLE_NECK: "거북목", + SHOULDER_TWIST: "어깨 틀어짐", + CHIN_UTP: "턱 괴기", + TAILBONE_SIT: "꼬리뼈로 앉기", +} + +const PoseAnalysisChart = ({ data }: { data: any[] }) => { + const options = { + tooltip: { + trigger: "axis", + axisPointer: { + type: "shadow", + }, + }, + legend: { + data: Object.values(poseTypeLabels), + top: 20, + }, + grid: { + left: "3%", + right: "3%", + bottom: "5%", + top: "25%", + containLabel: true, + }, + xAxis: { + type: "category", + data: data.map((item) => item.date), + }, + yAxis: { + type: "value", + minInterval: 1, + axisLabel: { + formatter: "{value}", + }, + }, + series: Object.keys(poseTypeLabels).map((poseType) => ({ + name: poseTypeLabels[poseType], + type: "line", + data: data.map((item) => { + const poseCount = item.count.find((c: any) => c.type === poseType) + return poseCount ? poseCount.count : 0 + }), + })), + } + + return +} + +export default PoseAnalysisChart diff --git a/src/components/Login.tsx b/src/components/Login.tsx new file mode 100644 index 0000000..53acaa3 --- /dev/null +++ b/src/components/Login.tsx @@ -0,0 +1,17 @@ +const Login: React.FC = () => { + const REST_API_KEY = "84b401e74d5a879d3fedfa7ba4366c68" + const REDIRECT_URI = "http://localhost:3000/auth" + const link = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code` + + const loginHandler = (): void => { + window.location.href = link + } + + return ( + + ) +} + +export default Login diff --git a/src/components/Modal/CreateCrewModal.tsx b/src/components/Modal/CreateCrewModal.tsx new file mode 100644 index 0000000..9354dee --- /dev/null +++ b/src/components/Modal/CreateCrewModal.tsx @@ -0,0 +1,177 @@ +import ModalContainer from "@components/ModalContainer" +import CheckedIcon from "@assets/icons/crew-checked-icon.svg?react" +import UnCheckedIcon from "@assets/icons/crew-unckecked-icon.svg?react" +import { useState } from "react" +import { ModalProps } from "@/contexts/ModalsContext" +import { useCheckGroupName, useCreateGroup } from "@/hooks/useGroupMutation" +import { group } from "@/api" + +type TPossible = "POSSIBLE" | "IMPOSSIBLE" | "NONCHECKED" + +const CreateCrewModal = (props: ModalProps): React.ReactElement => { + const { onClose, onSubmit } = props + + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [isHidden, setIsHidden] = useState(false) + const [joinCode, setJoinCode] = useState("") + const [isPossible, setIsPossible] = useState(null) + + const checkGroupNameMutation = useCheckGroupName() + const createGroupMutation = useCreateGroup() + + const onChangeName = (e: React.ChangeEvent): void => { + setName(e.target.value) + setIsPossible(null) + } + + const onChangeDescription = (e: React.ChangeEvent): void => { + if (e.target.value.length <= 300) setDescription(e.target.value) + } + + // Enter 키 입력을 막는 함수 + const handleKeyPress = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + e.preventDefault() // Enter 키 입력 방지 + } + } + + const onChangeJoinCode = (e: React.ChangeEvent): void => { + const { value } = e.target + // 숫자만 남기고 업데이트 + if (/^\d*$/.test(value)) { + if (value.length <= 4) setJoinCode(value) + } + } + + const onCheckIsHidden = (): void => { + setIsHidden(!isHidden) + setJoinCode("") + } + + const onCheckGroupName = async (): Promise => { + const _isPossible = await checkGroupNameMutation.mutateAsync(name) + setIsPossible(_isPossible ? "POSSIBLE" : "IMPOSSIBLE") + } + + const getNameCheckedMsg = (_isPossible: TPossible | null): string => { + if (_isPossible === "POSSIBLE") return "사용가능한 크루명입니다." + if (_isPossible === "IMPOSSIBLE") return "이미 사용중인 크루명이에요. 다른 크루명을 사용해주세요." + if (_isPossible === "NONCHECKED") return "중복체크를 해주세요." + return "" + } + + const canCreate = (): string | boolean => { + return name && description && ((isHidden && joinCode.length === 4) || !isHidden) + } + + const handleSubmit = (): void => { + if (isPossible === null) { + setIsPossible("NONCHECKED") + return + } + if (isPossible === "NONCHECKED") return + + let newGroup: group = { name, description } + if (isHidden) newGroup = { ...newGroup, joinCode, isHidden } + createGroupMutation.mutate(newGroup, { + onSuccess: (): void => { + if (onSubmit && typeof onSubmit === "function") onSubmit() + }, + }) + } + + return ( + +
+ {/* header */} +
+
{"크루 만들기"}
+
+ +
+ {/* crew owner */} +
+
크루명
+
+ + +
+
+ {getNameCheckedMsg(isPossible)} +
+
+ + {/* crew description */} +
+
크루 소개
+
+