diff --git a/next.config.mjs b/next.config.mjs index d5456a15..49ba09f8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,23 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + images: { + domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com', 'localhost'], + remotePatterns: [ + { + protocol: 'https', + hostname: 'via.placeholder.com', + port: '', + pathname: '/**', + }, + ], + }, + rewrites: async () => [ + { + source: '/api/proxy/:path*', + destination: 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/:path*', + }, + ], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 9fb61474..ce9380b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-radio-group": "^1.2.0", @@ -23,6 +24,8 @@ "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "formik": "^2.4.6", "lucide-react": "^0.402.0", "next": "14.2.4", "qs": "^6.12.2", @@ -33,6 +36,7 @@ "sharp": "^0.33.4", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", + "yup": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": { @@ -1918,6 +1922,15 @@ "react": "^18 || ^19" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1942,8 +1955,7 @@ "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/qs": { "version": "6.9.15", @@ -1955,7 +1967,6 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2848,8 +2859,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2908,6 +2918,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -2963,6 +2982,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4173,6 +4200,30 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4498,6 +4549,19 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/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==" + }, "node_modules/husky": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", @@ -5162,8 +5226,12 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6180,6 +6248,11 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6250,6 +6323,11 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/react-hook-form": { "version": "7.52.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", @@ -7112,6 +7190,16 @@ "node": ">=0.8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7123,6 +7211,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -7615,6 +7708,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index a84b4b59..450b39fe 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-radio-group": "^1.2.0", @@ -28,6 +29,8 @@ "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "formik": "^2.4.6", "lucide-react": "^0.402.0", "next": "14.2.4", "qs": "^6.12.2", @@ -38,6 +41,7 @@ "sharp": "^0.33.4", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", + "yup": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/public/icon/arrow-bottom-icon.svg b/public/icon/arrow-bottom-icon.svg new file mode 100644 index 00000000..8e0e4d20 --- /dev/null +++ b/public/icon/arrow-bottom-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icon/arrow-right-icon.svg b/public/icon/arrow-right-icon.svg new file mode 100644 index 00000000..dc5959d5 --- /dev/null +++ b/public/icon/arrow-right-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/apis/emotion.ts b/src/apis/emotion.ts new file mode 100644 index 00000000..8158e5a1 --- /dev/null +++ b/src/apis/emotion.ts @@ -0,0 +1,12 @@ +import { GetMonthlyEmotionLogsRequestType, GetMonthlyEmotionLogsResponseType } from '@/schema/emotion'; + +import httpClient from '.'; + +const getMonthlyEmotionLogs = async (request: GetMonthlyEmotionLogsRequestType): Promise => { + const response = await httpClient.get(`/emotionLogs/monthly`, { + params: request, + }); + return response.data; +}; + +export default getMonthlyEmotionLogs; diff --git a/src/apis/index.ts b/src/apis/index.ts index 6e81823b..0a4b7625 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import qs from 'qs'; +// NOTE: axios 선언 const httpClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_BASE_URL, - headers: { 'Content-Type': 'application/json' }, paramsSerializer: (parameters) => qs.stringify(parameters, { arrayFormat: 'repeat', encode: false }), }); diff --git a/src/apis/queries.ts b/src/apis/queries.ts index 1f84081c..0010c375 100644 --- a/src/apis/queries.ts +++ b/src/apis/queries.ts @@ -1,6 +1,8 @@ import { createQueryKeyStore } from '@lukemorales/query-key-factory'; import { GetUserRequestType } from '@/schema/user'; +import { GetMonthlyEmotionLogsRequestType } from '@/schema/emotion'; import { getMe, getUser } from './user'; +import getMonthlyEmotionLogs from './emotion'; const quries = createQueryKeyStore({ user: { @@ -13,6 +15,12 @@ const quries = createQueryKeyStore({ queryFn: () => getUser(request), }), }, + emotion: { + getMonthlyEmotionLogs: (request: GetMonthlyEmotionLogsRequestType) => ({ + queryKey: ['getMonthlyEmotionLogs', request], + queryFn: () => getMonthlyEmotionLogs(request), + }), + }, }); export default quries; diff --git a/src/apis/user.ts b/src/apis/user.ts index cd192766..5f924dea 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,4 +1,4 @@ -import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user'; +import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType } from '@/schema/user'; import httpClient from '.'; export const getMe = async (): Promise => { @@ -16,3 +16,10 @@ export const updateMe = async (request: PatchMeRequestType): Promise => { + const formData = new FormData(); + formData.append('image', request.image); + const response = await httpClient.post('/images/upload', formData); + return response.data; +}; diff --git a/src/components/Emotion/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx index fc92728b..4375c3ca 100644 --- a/src/components/Emotion/EmotionSelector.tsx +++ b/src/components/Emotion/EmotionSelector.tsx @@ -3,7 +3,7 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import EmotionIconCard from '@/components/Emotion/EmotionCard'; import { EmotionType, EmotionState } from '@/types/emotion'; import usePostEmotion from '@/hooks/usePostEmotion'; -import useGetEmotion from '@/hooks/useGetEmotion'; +import { useGetEmotion } from '@/hooks/useGetEmotion'; import EmotionSaveToast from './EmotionSaveToast'; /** diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 00000000..f44152fd --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import cn from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +function DialogHeader({ className, ...props }: React.HTMLAttributes) { + return
; +} +DialogHeader.displayName = 'DialogHeader'; + +function DialogFooter({ className, ...props }: React.HTMLAttributes) { + return
; +} +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; diff --git a/src/hooks/useCalendar.ts b/src/hooks/useCalendar.ts new file mode 100644 index 00000000..44afc6a6 --- /dev/null +++ b/src/hooks/useCalendar.ts @@ -0,0 +1,67 @@ +import { getDaysInMonth } from 'date-fns'; +import { CALENDAR_LENGTH, DAY_OF_WEEK } from '../user/utill/constants'; + +interface CalendarData { + weekCalendarList: number[][]; // 주별 날짜 리스트 +} + +// 이전 달의 날짜를 계산하는 함수 +const getPreviousDays = (firstDayOfCurrentMonth: Date, totalPrevMonthDays: number): number[] => + // 현재 월의 첫 번째 날의 요일을 기준으로 이전 달의 날짜를 배열로 반환 + Array.from({ length: firstDayOfCurrentMonth.getDay() }, (_, index) => totalPrevMonthDays - firstDayOfCurrentMonth.getDay() + index + 1); +// 현재 월의 날짜를 배열로 반환하는 함수 +const getCurrentDays = (totalMonthDays: number): number[] => Array.from({ length: totalMonthDays }, (_, i) => i + 1); // 1부터 totalMonthDays까지의 배열 생성 +// 다음 달의 날짜를 계산하는 함수 +const getNextDays = (currentDayList: number[], prevDayList: number[]): number[] => { + // 다음 달의 날짜 수를 계산하여 배열로 반환 + const nextDayCount = CALENDAR_LENGTH - currentDayList.length - prevDayList.length; + return Array.from({ length: Math.max(nextDayCount, 0) }, (_, index) => index + 1); +}; + +const useCalendar = (currentDate: Date): CalendarData => { + // 현재 월의 총 날짜 수를 가져옴 + const totalMonthDays = getDaysInMonth(currentDate); + + // 이전 달의 마지막 날짜를 계산 + const prevMonthLastDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0); + // 이전 달의 총 날짜 수를 가져옴 + const totalPrevMonthDays = getDaysInMonth(prevMonthLastDate); + + // 현재 월의 첫 번째 날짜를 계산 + const firstDayOfCurrentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); + // 이전 달의 날짜 리스트 + const prevDayList = getPreviousDays(firstDayOfCurrentMonth, totalPrevMonthDays); + // 현재 월의 날짜 리스트 + const currentDayList = getCurrentDays(totalMonthDays); + // 다음 달의 날짜 리스트 + const nextDayList = getNextDays(currentDayList, prevDayList); + + // 전체 날짜 리스트 (이전 / 현재 / 다음 달 날짜 포함) + const currentCalendarList = [...prevDayList, ...currentDayList, ...nextDayList]; + + // 주별로 날짜 리스트를 분할 + const weekCalendarList: number[][] = []; + currentCalendarList.forEach((currDate, index) => { + const chunkIndex = Math.floor(index / DAY_OF_WEEK); + if (!weekCalendarList[chunkIndex]) { + weekCalendarList[chunkIndex] = []; // 주 배열이 없으면 초기화 + } + weekCalendarList[chunkIndex].push(currDate); // 누적값 반환 + }); + + // NOTE: 한 달이 5주 일 수도, 6주 일 수도 있을 때 5주인 경우 해당 달에 필요없는 다음 달의 날짜가 출력되기 때문에 (CALENDAR_LENGTH를 최대치인 42로 잡아서) 마지막 주의 첫 번째 숫자가 10이하의 날짜로 시작한다면 해당 배열을 삭제하도록 추가. + // TODO: 추후 다른 방법이 있다면 변경 할 예정 + if (weekCalendarList.length > 0) { + const lastWeek = weekCalendarList[weekCalendarList.length - 1]; + if (lastWeek[0] <= 10) { + weekCalendarList.pop(); + } + } + + // 캘린더 정보를 반환 + return { + weekCalendarList, // 주별 날짜 리스트 + }; +}; + +export default useCalendar; diff --git a/src/hooks/useGetEmotion.ts b/src/hooks/useGetEmotion.ts index d8017abc..e8555101 100644 --- a/src/hooks/useGetEmotion.ts +++ b/src/hooks/useGetEmotion.ts @@ -1,11 +1,13 @@ -import { useQuery } from '@tanstack/react-query'; +import quries from '@/apis/queries'; import getEmotion from '@/apis/getEmotion'; import { EmotionType } from '@/types/emotion'; +import { GetMonthlyEmotionLogsRequestType } from '@/schema/emotion'; +import { useQuery } from '@tanstack/react-query'; -const useGetEmotion = () => +export const useMonthlyEmotionLogs = (requset: GetMonthlyEmotionLogsRequestType) => useQuery(quries.emotion.getMonthlyEmotionLogs(requset)); + +export const useGetEmotion = () => useQuery({ queryKey: ['emotion'], queryFn: getEmotion, }); - -export default useGetEmotion; diff --git a/src/hooks/userQueryHooks.ts b/src/hooks/userQueryHooks.ts index 29ba4e2e..f5043b15 100644 --- a/src/hooks/userQueryHooks.ts +++ b/src/hooks/userQueryHooks.ts @@ -1,6 +1,6 @@ import quries from '@/apis/queries'; -import { updateMe } from '@/apis/user'; -import { GetUserResponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user'; +import { updateMe, createPresignedUrl } from '@/apis/user'; +import { GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType } from '@/schema/user'; import { MutationOptions } from '@/types/query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -8,7 +8,7 @@ export const useMeQuery = () => useQuery(quries.user.getMe()); export const useUserQuery = (requset: GetUserRequestType) => useQuery(quries.user.getUser(requset)); -export const useUpdateMe = (options: MutationOptions) => { +export const useUpdateMe = (options: MutationOptions) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (request: PatchMeRequestType) => updateMe(request), @@ -21,3 +21,13 @@ export const useUpdateMe = (options: MutationOptions) => { }, }); }; + +// presignedUrl 생성 +export const useCreatePresignedUrl = (options?: MutationOptions) => + useMutation({ + mutationFn: (request: PostPresignedUrlRequestType) => createPresignedUrl(request), + ...options, + onSuccess: (data: PostPresignedUrlResponseType) => + // 이미지 URL 반환 + data.url, + }); diff --git a/src/pageLayout/MypageLayout/MyPageLayout.tsx b/src/pageLayout/MypageLayout/MyPageLayout.tsx new file mode 100644 index 00000000..12627ade --- /dev/null +++ b/src/pageLayout/MypageLayout/MyPageLayout.tsx @@ -0,0 +1,47 @@ +import Header from '@/components/Header/Header'; +import { useMeQuery } from '@/hooks/userQueryHooks'; +import UserInfo from '@/types/user'; +import EmotionMonthlyLogs from '@/user/ui-profile/EmotionMonthlyLogs'; +import Profile from '@/user/ui-profile/Profile'; +import { useRouter } from 'next/navigation'; + +export default function MyPageLayout() { + const { data, isLoading, isError }: { data: UserInfo | undefined; isLoading: boolean; isError: boolean } = useMeQuery(); + + const router = useRouter(); + + if (isError) { + return
error
; + } + + if (isLoading) { + return
loading
; + } + + // NOTE: 회원정보가 확인되지 않는다면 로그인 페이지로 이동 + if (!data) { + router.push('/login'); + return false; + } + + return ( +
+
{}} /> +
+
+ +
오늘의 감정
+ +
+
+
+
+

내 에피그램(19)

+

내 댓글(110)

+
+
댓글 컴포넌트
+
+
+
+ ); +} diff --git a/src/pages/mypage/index.tsx b/src/pages/mypage/index.tsx new file mode 100644 index 00000000..69b8c83e --- /dev/null +++ b/src/pages/mypage/index.tsx @@ -0,0 +1,5 @@ +import MyPageLayout from '@/pageLayout/MypageLayout/MyPageLayout'; + +export default function mypage() { + return ; +} diff --git a/src/schema/emotion.ts b/src/schema/emotion.ts index a2239be9..2bc5a92e 100644 --- a/src/schema/emotion.ts +++ b/src/schema/emotion.ts @@ -1,5 +1,25 @@ import * as z from 'zod'; +/** **************** 감정 달력 ***************** */ +export const GetMonthlyEmotionLogsRequest = z.object({ + userId: z.number(), + year: z.number(), + month: z.number(), +}); + +// 감정 로그 항목의 스키마 정의 +const EmotionSchema = z.object({ + id: z.number(), + userId: z.number(), + emotion: z.enum(['MOVED', 'HAPPY', 'WORRIED', 'SAD', 'ANGRY']), + createdAt: z.coerce.date(), +}); + +// 감정 로그 배열 정의 +export const GetMonthlyEmotionLogsResponse = z.array(EmotionSchema); +export type GetMonthlyEmotionLogsRequestType = z.infer; +export type GetMonthlyEmotionLogsResponseType = z.infer; + export const PostEmotionRequest = z.object({ emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), }); diff --git a/src/schema/user.ts b/src/schema/user.ts index 4e9fde85..5c6881cb 100644 --- a/src/schema/user.ts +++ b/src/schema/user.ts @@ -1,4 +1,5 @@ import * as z from 'zod'; +import { MAX_FILE_SIZE, ACCEPTED_IMAGE_TYPES } from '@/user/utill/constants'; export const PatchMeRequest = z.object({ image: z.string().url(), @@ -18,6 +19,20 @@ export const GetUserReponse = z.object({ id: z.number(), }); +const PostPresignedUrlRequest = z.object({ + image: z + .instanceof(File) + .refine((file) => file.size <= MAX_FILE_SIZE, `업로드 파일의 용량은 최대 ${MAX_FILE_SIZE / (1024 * 1024)}MB 입니다.`) + .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), '.jpg, .jpeg, .png 확장자만 업로드 가능합니다.'), +}); + +export const PostPresignedUrlResponse = z.object({ + url: z.string().url(), +}); + export type GetUserResponseType = z.infer; export type GetUserRequestType = z.infer; export type PatchMeRequestType = z.infer; + +export type PostPresignedUrlRequestType = z.infer; +export type PostPresignedUrlResponseType = z.infer; diff --git a/src/types/emotion.ts b/src/types/emotion.ts index e64f3d3f..45b5b78c 100644 --- a/src/types/emotion.ts +++ b/src/types/emotion.ts @@ -1,3 +1,19 @@ +export interface Emotion { + userId: number; + year: number; + month: number; +} + +// 감정 로그 타입 지정 +export type EmotionTypeEN = 'MOVED' | 'HAPPY' | 'WORRIED' | 'SAD' | 'ANGRY'; + +export interface EmotionLog { + id: number; + userId: number; + emotion: EmotionTypeEN; + createdAt: Date; +} + export type EmotionType = '감동' | '기쁨' | '고민' | '슬픔' | '분노'; export type EmotionState = 'Default' | 'Unclicked' | 'Clicked'; diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 00000000..d821ed31 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,13 @@ +export default interface UserInfo { + nickname: string; + image: string; + id: number; + updatedAt: Date; + createdAt: Date; + teamId: string; +} + +export interface UserProfileProps { + image: string; + nickname: string; +} diff --git a/src/user/ui-profile/Calendar.tsx b/src/user/ui-profile/Calendar.tsx new file mode 100644 index 00000000..4ce6d06a --- /dev/null +++ b/src/user/ui-profile/Calendar.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import { subMonths } from 'date-fns'; +import { EmotionLog, EmotionTypeEN } from '@/types/emotion'; +import useCalendar from '../../hooks/useCalendar'; +import { DAY_LIST, DATE_MONTH_FIXER, iconPaths } from '../utill/constants'; +import CalendarHeader from './CalendarHeader'; + +interface CalendarProps { + currentDate: Date; // 현재 날짜 + setCurrentDate: React.Dispatch>; // 현재 날짜를 설정하는 함수 + monthlyEmotionLogs: EmotionLog[]; +} + +export default function Calendar({ currentDate, setCurrentDate, monthlyEmotionLogs }: CalendarProps) { + // 캘린더 함수 호출 + const { weekCalendarList } = useCalendar(currentDate); + // 감정 필터 + const [selectedEmotion, setSelectedEmotion] = useState(null); + + // 달력에 출력할 수 있게 매핑 + const emotionMap: Record = Array.isArray(monthlyEmotionLogs) + ? monthlyEmotionLogs.reduce>((acc, log) => { + const date = new Date(log.createdAt); + const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + acc[dateString] = log.emotion as EmotionTypeEN; + return acc; + }, {}) + : {}; + + // 이전 달 클릭 + const handlePrevMonth = () => setCurrentDate((prevDate) => subMonths(prevDate, DATE_MONTH_FIXER)); + // 다음 달 클릭 + const handleNextMonth = () => setCurrentDate((prevDate) => subMonths(prevDate, -DATE_MONTH_FIXER)); + + // 감정 필터 + const handleEmotionSelect = (emotion: EmotionTypeEN) => { + // 현재 선택된 감정과 같으면 초기화 + if (selectedEmotion === emotion) { + setSelectedEmotion(null); + } else { + setSelectedEmotion(emotion); + } + }; + + // 필터링된 감정 맵 생성 + const filteredEmotionMap = selectedEmotion ? Object.fromEntries(Object.entries(emotionMap).filter(([, value]) => value === selectedEmotion)) : emotionMap; + + return ( +
+ {/* 캘린더 헤더 */} + + {/* 캘린더 */} +
+
+ {DAY_LIST.map((day) => ( +
+ {day} +
+ ))} +
+ {weekCalendarList.map((week, weekIndex) => ( + // TODO: index 값 Lint error. 임시로 주석 사용. 추후 수정 예정 + // eslint-disable-next-line react/no-array-index-key +
+ {week.map((day, dayIndex) => { + // 현재 날짜와 비교 + const isToday = day === currentDate.getDate() && currentDate.getMonth() === new Date().getMonth() && currentDate.getFullYear() === new Date().getFullYear(); + const dateString = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const emotion: EmotionTypeEN = filteredEmotionMap[dateString]; // 날짜에 해당하는 감정 가져오기 + const iconPath = emotion && iconPaths[emotion] ? iconPaths[emotion].path : '/icon/BW/SmileFaceBWIcon.svg'; + + return ( +
+ {emotion ? ( +
+

{day}

+ 감정 +
+ ) : ( +

{day}

+ )} +
+ ); + })} +
+ ))} +
+
+ ); +} diff --git a/src/user/ui-profile/CalendarHeader.tsx b/src/user/ui-profile/CalendarHeader.tsx new file mode 100644 index 00000000..2c337e1c --- /dev/null +++ b/src/user/ui-profile/CalendarHeader.tsx @@ -0,0 +1,58 @@ +import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenu } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import Image from 'next/image'; +import { EmotionTypeEN } from '@/types/emotion'; +import ARROW_BOTTOM_ICON from '../../../public/icon/arrow-bottom-icon.svg'; +import ARROW_RIGHT_ICON from '../../../public/icon/arrow-right-icon.svg'; +import ARROW_LEFT_ICON from '../../../public/icon/arrow-left-icon.svg'; +import { iconPaths } from '../utill/constants'; + +interface CalendarHeaderProps { + currentDate: Date; + onPrevMonth: () => void; + onNextMonth: () => void; + onEmotionSelect: (emotion: EmotionTypeEN) => void; + selectEmotion: EmotionTypeEN | null; +} + +export default function CalendarHeader({ currentDate, onPrevMonth, onNextMonth, onEmotionSelect, selectEmotion }: CalendarHeaderProps) { + return ( +
+
+
{`${currentDate.getFullYear()}년 ${currentDate.getMonth() + 1}월`}
+ + + + + + + {Object.entries(iconPaths).map(([emotionKey, { path, name }]) => ( + + + + ))} + + + +
+
+ + +
+
+ ); +} diff --git a/src/user/ui-profile/Chart.tsx b/src/user/ui-profile/Chart.tsx new file mode 100644 index 00000000..6e89af4c --- /dev/null +++ b/src/user/ui-profile/Chart.tsx @@ -0,0 +1,96 @@ +import { EmotionLog, EmotionTypeEN } from '@/types/emotion'; +import Image from 'next/image'; +import { iconPaths } from '../utill/constants'; + +interface ChartProps { + monthlyEmotionLogs: EmotionLog[]; +} + +export default function Chart({ monthlyEmotionLogs }: ChartProps) { + // 감정별 빈도수 계산 + const emotionCounts = monthlyEmotionLogs.reduce( + (count, log) => { + const { emotion } = log; + return { + ...count, // 기존의 count를 복사 + [emotion]: (count[emotion] || 0) + 1, // 현재 감정의 개수 증가 + }; + }, + {} as Record, + ); + + // 감정 종류 및 총 감정 수 계산 + const TOTAL_COUNT = monthlyEmotionLogs.length; + const EMOTIONS: EmotionTypeEN[] = ['MOVED', 'HAPPY', 'WORRIED', 'SAD', 'ANGRY']; + const RADIUS = 90; // 원의 반지름 + const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + + // 가장 많이 나타나는 감정 찾기 + const maxEmotion = EMOTIONS.reduce((max, emotion) => (emotionCounts[emotion] > emotionCounts[max] ? emotion : max), EMOTIONS[0]); + + // 원형 차트의 각 감정에 대한 strokeDasharray와 strokeDashoffset 계산 + let offset = 0; + + return ( +
+

감정 차트

+
+
+ + + {EMOTIONS.map((emotion) => { + const count = emotionCounts[emotion] || 0; + const percentage = TOTAL_COUNT > 0 ? count / TOTAL_COUNT : 0; // 0으로 나누기 방지 + const strokeDasharray = `${CIRCUMFERENCE * percentage} ${CIRCUMFERENCE * (1 - percentage)}`; + + // 색상 설정 + let strokeColor; + switch (emotion) { + case 'HAPPY': + strokeColor = '#FBC85B'; + break; + case 'SAD': + strokeColor = '#E3E9F1'; + break; + case 'WORRIED': + strokeColor = '#C7D1E0'; + break; + case 'ANGRY': + strokeColor = '#EFF3F8'; + break; + default: + strokeColor = '#48BB98'; + } + + const circle = ; + + offset += CIRCUMFERENCE * percentage; // 다음 원을 위한 offset 업데이트 + return circle; + })} + + {/* 중앙에 가장 많이 나타나는 감정 출력 */} +
+ 감정 +

{iconPaths[maxEmotion].name}

+
+
+
+
+ {EMOTIONS.map((emotion) => { + const count = emotionCounts[emotion] || 0; + const percentage = TOTAL_COUNT > 0 ? Math.floor((count / TOTAL_COUNT) * 100) : 0; // 퍼센트 계산 및 소수점 버리기 + + return ( +
+

+ 감정 +

{percentage}%

+
+ ); + })} +
+
+
+
+ ); +} diff --git a/src/user/ui-profile/EmotionMonthlyLogs.tsx b/src/user/ui-profile/EmotionMonthlyLogs.tsx new file mode 100644 index 00000000..18d4ada8 --- /dev/null +++ b/src/user/ui-profile/EmotionMonthlyLogs.tsx @@ -0,0 +1,40 @@ +import { useMonthlyEmotionLogs } from '@/hooks/useGetEmotion'; +import { Emotion } from '@/types/emotion'; +import { useEffect, useState } from 'react'; +import Calendar from './Calendar'; +import Chart from './Chart'; + +interface EmotionMonthlyLogsProps { + userId: number; +} + +export default function EmotionMonthlyLogs({ userId }: EmotionMonthlyLogsProps) { + // 현재 날짜를 상태로 관리 + const [currentDate, setCurrentDate] = useState(new Date()); + + // 감정 달력 객체 상태 추가 + const [emotionRequest, setEmotionRequest] = useState({ + userId, + year: currentDate.getFullYear(), + month: currentDate.getMonth() + 1, + }); + + // '월'이 변경될 때마다 request 업데이트 + useEffect(() => { + setEmotionRequest({ + userId, + year: currentDate.getFullYear(), + month: currentDate.getMonth() + 1, + }); + }, [currentDate]); + + // 월별 감정 로그 조회 + const { data: monthlyEmotionLogs = [] } = useMonthlyEmotionLogs(emotionRequest); + + return ( + <> + + + + ); +} diff --git a/src/user/ui-profile/Profile.tsx b/src/user/ui-profile/Profile.tsx new file mode 100644 index 00000000..42fd5f74 --- /dev/null +++ b/src/user/ui-profile/Profile.tsx @@ -0,0 +1,41 @@ +import Image from 'next/image'; +import { UserProfileProps } from '@/types/user'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog'; +import { sampleImage } from '../utill/constants'; +import ProfileEdit from './ProfileEdit'; + +export default function Profile({ image, nickname }: UserProfileProps) { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleProfileEditClose = () => { + setIsModalOpen(false); + }; + + // TODO: 여러개의 샘플 이미지 랜덤하게 뜨도록 추가 할 예정 + const profileImage = image || sampleImage; + + return ( +
+
+
+ 유저 프로필 +
+
+

{nickname}

+
+ + + + + + + + +
+
+ ); +} diff --git a/src/user/ui-profile/ProfileEdit.tsx b/src/user/ui-profile/ProfileEdit.tsx new file mode 100644 index 00000000..218f2c7a --- /dev/null +++ b/src/user/ui-profile/ProfileEdit.tsx @@ -0,0 +1,140 @@ +import Image from 'next/image'; +import { UserProfileProps } from '@/types/user'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import Label from '@/components/ui/label'; +import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { useToast } from '@/components/ui/use-toast'; +import { useEffect, useRef } from 'react'; +import { Form, Formik, useFormik } from 'formik'; +import { useCreatePresignedUrl, useUpdateMe } from '@/hooks/userQueryHooks'; +import * as Yup from 'yup'; +import { AxiosError } from 'axios'; +import fileNameChange from '../utill/fileNameChange'; + +interface UserProfileEditProps { + initialValues: { + image: string; + nickname: string; + }; + onModalClose: () => void; +} + +const validationSchema = Yup.object().shape({ + nickname: Yup.string().min(1, '닉네임은 1자 이상 30자 이하여야 합니다.').max(30, '닉네임은 1자 이상 30자 이하여야 합니다.').required('닉네임은 필수 항목입니다.'), +}); + +export default function ProfileEdit({ initialValues, onModalClose }: UserProfileEditProps) { + const createPresignedUrl = useCreatePresignedUrl(); + const fileInputRef = useRef(null); + + const { toast } = useToast(); + + const handleSubmit = async () => { + await formik.submitForm(); // Formik의 submitForm 함수 호출 + }; + + const { mutate: updateMe } = useUpdateMe({ + onSuccess: () => { + onModalClose(); + toast({ + description: '프로필 수정이 완료되었습니다.', + }); + }, + onError: () => { + toast({ + description: '프로필 수정 실패', + }); + }, + }); + + const formik = useFormik({ + initialValues: { + image: '', + nickname: '', + }, + validationSchema, + onSubmit: async (values, { setSubmitting }) => { + try { + // 프로필 업데이트 + await updateProfile(values); + setSubmitting(false); + } catch (error) { + // 에러 처리 + } finally { + setSubmitting(false); + } + }, + }); + + const updateProfile = (values: UserProfileProps) => { + updateMe(values); + }; + + // 프로필 사진 변경 클릭 + const handleImageEditClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + // 이미지 변경 시 + async function handleImageChange(e: React.ChangeEvent): Promise { + const { files } = e.currentTarget; + if (files && files.length > 0) { + const file = files[0]; + + try { + // 중복된 파일명 및 한글파일이 저장되지 않도록 파일이름 포멧 변경 + const newFileName = fileNameChange(); + const newFile = new File([file], `${newFileName}.${file.name.split('.').pop()}`, { type: file.type }); + + // presignedUrl 구하는 함수 (s3 업로드까지 같이) + const { url } = await createPresignedUrl.mutateAsync({ image: newFile }); + formik.setFieldValue('image', url); + } catch (error) { + // 에러 처리: 실패 시 토스트 메시지 + const axiosError = error as AxiosError; + + onModalClose(); + const errorMessage = `(error: ${axiosError.response?.status}) 잘못 된 요청입니다. 관리자에게 문의해주세요`; + + toast({ + description: errorMessage, + className: 'bg-red-400 text-white', + }); + } + } + } + + useEffect(() => { + formik.setValues(initialValues); + }, [initialValues]); + + return ( + + {({ isSubmitting }) => ( +
+ + 프로필 수정 +
+
+ 유저 프로필 + handleImageChange(e)} className='hidden' ref={fileInputRef} /> +
+
+ + +
+
+ + + +
+
+ )} +
+ ); +} diff --git a/src/user/utill/constants.ts b/src/user/utill/constants.ts new file mode 100644 index 00000000..9ff423c8 --- /dev/null +++ b/src/user/utill/constants.ts @@ -0,0 +1,20 @@ +// 파일 업로드 관련 +export const MAX_FILE_SIZE = 1024 * 1024 * 5; // 파일 업로드 최대 용량 5MB +export const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png']; // 허용 가능 확장자 +export const sampleImage = '/ProfileTestImage.jpg'; // 초기프로필 이미지 + +// 캘린더 관련 상수 +export const DAY_LIST = ['일', '월', '화', '수', '목', '금', '토']; // 요일 +export const DATE_MONTH_FIXER = 1; // 날짜 조정 상수 (현재 사용되지 않음, 필요에 따라 활용 가능) +export const CALENDAR_LENGTH = 42; // 6주에 맞추어 캘린더의 총 길이를 42로 설정 +export const DAY_OF_WEEK = 7; // 한 주의 날 수 (일~토) +export const DEFAULT_TRASH_VALUE = -1; // 기본값 설정 (필요에 따라 사용 가능) + +// 아이콘 파일 경로 매핑 +export const iconPaths = { + MOVED: { path: '/icon/Color/HeartFaceColorIcon.svg', name: '기쁨', color: 'bg-illust-green' }, + HAPPY: { path: '/icon/Color/SmileFaceColorIcon.svg', name: '감동', color: 'bg-illust-yellow' }, + WORRIED: { path: '/icon/Color/ThinkFaceColorIcon.svg', name: '고민', color: 'bg-sub-gray_1' }, + SAD: { path: '/icon/Color/SadFaceColorIcon.svg', name: '슬픔', color: 'bg-sub-gray_2' }, + ANGRY: { path: '/icon/Color/AngryFaceColorIcon.svg', name: '분노', color: 'bg-sub-gray_3' }, +}; diff --git a/src/user/utill/fileNameChange.ts b/src/user/utill/fileNameChange.ts new file mode 100644 index 00000000..73007d0c --- /dev/null +++ b/src/user/utill/fileNameChange.ts @@ -0,0 +1,8 @@ +function fileNameChange() { + const now = new Date(); + const formattedFileName = `profile${now.getHours()}${now.getMinutes()}${now.getSeconds()}${now.getDate()}${now.getMonth() + 1}${now.getFullYear()}`; + + return formattedFileName; +} + +export default fileNameChange; diff --git a/tailwind.config.js b/tailwind.config.js index 25a4545d..a8b08d2c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -50,6 +50,9 @@ module.exports = { 'sub-gray_2': '#E3E9F1', 'sub-gray_3': '#EFF3F8', }, + boxShadow: { + '3xl': '0px 0px 36px 0px rgba(0, 0, 0, 0.05)', + }, screens: { sm: '640px', md: '768px',