diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..33ac780 --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +# VUE_APP_API_URL=http://localhost:8081 +VUE_APP_DEVTOOLS=true +VUE_APP_API_URL=http://223.130.155.196:8080/api diff --git a/.env.production b/.env.production index 5ae0862..041ce37 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,4 @@ -VUE_APP_API_URL=http://223.130.155.196:8080/api/ \ No newline at end of file +# VUE_APP_API_URL=http://localhost:8081 +VUE_APP_API_URL=http://223.130.155.196:8080/api +VUE_APP_ENABLE_DEVTOOLS=true + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1f02d94..dc3b02c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,4 +71,5 @@ jobs: docker rm vue_frontend || true docker run -d -p 80:3000 --name vue_frontend \ -e VUE_APP_API_URL=${{ secrets.VUE_APP_API_URL }} \ + -e VUE_APP_DEVTOOLS=true \ ${{ env.DOCKER_IMAGE_NAME }}:latest diff --git a/.gitignore b/.gitignore index 4634a8e..b52e3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + .env.development diff --git a/package-lock.json b/package-lock.json index d8100db..9b1693c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,14 @@ "dependencies": { "axios": "^1.6.8", "core-js": "^3.8.3", + "mitt": "^3.0.1", "moment": "^2.30.1", "swiper": "^11.1.3", "vue": "^3.4.23", "vue-click-outside": "^1.1.0", "vue-router": "^4.3.2", - "vuex": "^4.0.0" + "vuex": "^4.0.0", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -6385,7 +6387,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12598,6 +12599,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -14865,6 +14871,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shvl": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz", + "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==", + "deprecated": "older versions vulnerable to prototype pollution" + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -16281,6 +16293,19 @@ "vue": "^3.2.0" } }, + "node_modules/vuex-persistedstate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz", + "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "deepmerge": "^4.2.2", + "shvl": "^2.0.3" + }, + "peerDependencies": { + "vuex": "^3.0 || ^4.0.0-rc" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 0c46979..19d75f8 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,21 @@ "private": true, "scripts": { "serve": "vue-cli-service serve --mode development", - "build": "vue-cli-service build --mode production", + "build": "vue-cli-service build --mode development", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint" }, "dependencies": { "axios": "^1.6.8", "core-js": "^3.8.3", + "mitt": "^3.0.1", "moment": "^2.30.1", "swiper": "^11.1.3", "vue": "^3.4.23", "vue-click-outside": "^1.1.0", "vue-router": "^4.3.2", - "vuex": "^4.0.0" + "vuex": "^4.0.0", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@babel/core": "^7.12.16", diff --git a/src/App.vue b/src/App.vue index e660314..40dc14f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,23 +1,57 @@ diff --git a/src/api/common/interceptors.js b/src/api/common/interceptors.js index f6e1ff9..c5a68e9 100644 --- a/src/api/common/interceptors.js +++ b/src/api/common/interceptors.js @@ -1,12 +1,18 @@ -import store from '@/store/index'; // 스토어 모듈을 가져옵니다. +import axios from 'axios'; +import store from '@/store/index'; +import { + saveAccessTokenToCookie, + getRefreshTokenFromCookie, +} from '@/utils/cookies'; export function setInterceptors(instance) { // 요청 인터셉터를 추가합니다. instance.interceptors.request.use( function (config) { - // 요청을 보내기 전에 수행할 작업입니다. - // console.log(config); // 요청(config)을 로깅합니다. - config.headers.Authorization = store.state.token; // 요청 헤더에 토큰을 추가합니다. + const token = store.state.accessToken; // Vuex 스토어에서 액세스 토큰을 가져옵니다. + if (token) { + config.headers.Authorization = `Bearer ${token}`; // 요청 헤더에 토큰을 추가합니다. + } return config; // 수정된 요청(config)을 반환합니다. }, function (error) { @@ -19,12 +25,37 @@ export function setInterceptors(instance) { instance.interceptors.response.use( function (response) { // 2xx 범위 내의 모든 상태 코드에 대해 이 함수가 실행됩니다. - // 응답 데이터 처리를 위한 작업을 수행합니다. return response; // 응답을 반환합니다. }, - function (error) { - // 2xx 범위 외의 모든 상태 코드에 대해 이 함수가 실행됩니다. - // 응답 에러 처리를 위한 작업을 수행합니다. + async function (error) { + const { + config, + response: { status }, + } = error; + const originalRequest = config; + if ( + error.response && + error.response.data && + error.response.data.message + ) { + store.commit('setErrorMessage', error.response.data.message); + } + if (status === 401 && !error.response.data.message) { + // 액세스 토큰이 만료된 경우 새로운 액세스 토큰을 발급받습니다. + try { + const refreshToken = getRefreshTokenFromCookie(); // 쿠키에서 리프레시 토큰을 가져옵니다. + const { data } = await axios.post( + `${process.env.VUE_APP_API_URL}/api/token/access-token`, + { refreshToken }, + ); + store.commit('setAccessToken', data.accessToken); // 새로운 액세스 토큰을 Vuex 스토어에 저장합니다. + saveAccessTokenToCookie(data.accessToken); // 새로운 액세스 토큰을 쿠키에 저장합니다. + originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; // 요청 헤더를 갱신된 토큰으로 수정합니다. + return axios(originalRequest); // 원래의 요청을 재시도합니다. + } catch (e) { + store.dispatch('logout'); // 리프레시 토큰 갱신 실패 시 로그아웃 처리합니다. + } + } return Promise.reject(error); // 에러를 반환합니다. }, ); diff --git a/src/api/index.js b/src/api/index.js index 411e72d..8d5f43c 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,5 +1,6 @@ // HTTP 요청을 보내기 위해 'axios' 라이브러리를 가져옵니다. import axios from 'axios'; +// import store from '@/store/index'; // './common/interceptors' 모듈에서 'setInterceptors' 함수를 가져옵니다. import { setInterceptors } from './common/interceptors'; @@ -11,7 +12,6 @@ function createInstance() { baseURL: process.env.VUE_APP_API_URL, // 인스턴스의 기본 URL을 설정합니다. withCredentials: true, }); - // 인터셉터를 인스턴스에 적용합니다. return setInterceptors(instance); } @@ -21,34 +21,32 @@ const instance = createInstance(); // Reward API // 특정 사용자의 보상 프로세스를 시작하는 함수입니다. -function rewardStart(userId) { +function rewardStart(email) { // 환경 변수에서 가져온 기본 URL을 로깅합니다. console.log('Base URL:', process.env.VUE_APP_API_URL); // 특정 사용자의 보상 프로세스를 시작하는 요청 URL을 로깅합니다. - console.log('Request URL:', `/reward-process/${userId}/start`); - - + console.log('Request URL:', `/reward-process/${email}/start`); // 사용자의 보상 프로세스를 시작하기 위해 GET 요청을 보냅니다. - return instance.get(`/reward-process/${userId}/start`); + return instance.get(`/reward-process/${email}/start`); } -function issueCouponAPI(userId, couponId) { +function issueCouponAPI(email, couponId) { // 특정 사용자와 쿠폰 ID에 대한 쿠폰 발급 시도를 로깅합니다. - console.log('Attempting to issue coupon:', { userId, couponId }); + console.log('Attempting to issue coupon:', { email, couponId }); // 사용자에게 쿠폰을 발급하기 위해 POST 요청을 보냅니다. return instance.post(`/coupon/issue`, { - userId, + email, couponId, }); } // 사용자의 쿠폰 목록을 검색하는 함수입니다. -function myCouponList(userId) { +function myCouponList(email) { // 사용자의 쿠폰 목록을 검색하기 위해 GET 요청을 보냅니다. - return instance.get(`/coupon/${userId}/coupon/list`); + return instance.get(`/coupon/${email}/coupon/list`); } // 쿠폰을 사용 처리하는 함수입니다. @@ -57,30 +55,58 @@ function CouponUse(couponHistoryId) { return instance.get(`/coupon-history/${couponHistoryId}/use`); } -function RewardProcessCheck(userId) { - return instance.get(`/reward-process/${userId}/get`); +function RewardProcessCheck(email) { + return instance.get(`/reward-process/${email}/get`); } -function ReviewCount(userId) { - return instance.patch(`/reward-process/${userId}/review`); +function ReviewCount(email) { + return instance.patch(`/reward-process/${email}/review`); } -function resetStatus(userId) { - return instance.patch(`/reward-process/${userId}/new`); +function resetStatus(email) { + return instance.patch(`/reward-process/${email}/new`); } -function rewardFinish(userId) { - return instance.patch(`/reward-process/${userId}/completed`); +function rewardFinish(email) { + return instance.patch(`/reward-process/${email}/completed`); } -function rewardList(userId) { - return instance.get(`/reward-history/user/${userId}/list`); +function rewardList(email) { + return instance.get(`/reward-history/user/${email}/list`); } -function rewardAdd(userId) { +function rewardAdd(email) { const today = new Date().getMonth() + 1; return instance.post(`/rewards/obtain`, { - member_id: userId, + member_email: email, reward_id: today, }); } +// axios 인스턴스가 'instance'로 정의되어 있다고 가정합니다. +function getStoreKeywords(storeId) { + return instance.get(`/store/${storeId}/keywords`); +} + +function likeReview(reviewId, userId) { + return instance.post(`/review-likes/${reviewId}/${userId}`); +} + +async function addFollowUser(userName, userEmail) { + console.log('username : ' + userName); + try { + const response = await instance.post( + `/reviews/addFollowUser?userName=${encodeURIComponent( + userName, + )}&userEmail=${encodeURIComponent(userEmail)}`, + ); + + if (response.status === 200) { + console.log(response); + return response; + } else { + throw new Error('팔로우하는데 실패했습니다.'); + } + } catch (error) { + console.error('오류:', error); + } +} // 백엔드에서 보내는 매장 목록을 받아오는 fetchStoreList 함수를 추가합니다. async function fetchStoreList() { @@ -91,10 +117,19 @@ async function fetchStoreList() { throw new Error('Error fetching store list: ' + error.message); } } - -async function selectStore(storeId, page = 0, size = 20) { +async function selectStore( + storeId, + userEmail, + page = 0, + size = 10, + filter = '', + sort, + keyword = '', +) { try { - const response = await instance.get(`/store/${storeId}/reviews?page=${page}&size=${size}`); + const response = await instance.get(`/store/${storeId}/reviews`, { + params: { userEmail, page, size, filter, sort, keyword }, + }); if (response.status === 200) { const storeWithReviewData = response.data; console.log(storeWithReviewData); @@ -107,12 +142,76 @@ async function selectStore(storeId, page = 0, size = 20) { } } -async function getAllReview(page = 0, size = 20) { +// 다른 모듈에서 접근할 수 있도록 함수들을 내보냅니다. + +// 로그인 API +function loginUser(userData) { + return instance.post('/user/login', userData); +} +// 회원가입 API +function registerUser(userData) { + return instance.post('/user/join', userData); +} +// 닉네임 중복 검사 API +function checkNicknameDuplicate(nickname) { + return instance.post('/user/join/nickname-check', { nickname }); +} +// 이메일 중복 검사 API +function checkEmailDuplicate(email) { + return instance.post('/user/join/email-check', { email }); +} +// 이메일 인증 코드 전송 API +function sendAuthCode(email) { + return instance.post('/auth/send-code', { email }); +} +// 이메일 인증 코드 검증 API +function verifyAuthCode(email, code) { + return instance.post('/auth/verify-code', { email, code }); +} +// 비밀번호 업데이트 API +function updatePassword(email, newPassword) { + return instance.post('/user/update-password', { + email: email, + newPassword: newPassword, + }); +} +// OAuth2 로그인 API +function loginWithOAuth2(provider, code) { + return instance.post(`/oauth2/${provider}`, { code }); +} +// 사용자 정보를 가져오는 API +function fetchUserData(email) { + return instance.post('/user/details', { email }); +} +// 사용자 프로필 업데이트 API +function updateUserProfile(profileData) { + return instance.post('/user/update-profile', profileData); +} +// 액세스 토큰을 새로 발급받는 API +function getNewAccessToken(refreshToken) { + return instance.post('/api/token/access-token', { refreshToken }); +} +// 로그아웃 API +function logoutUser(logoutData) { + return instance.post('/user/logout', logoutData, { + headers: { + Authorization: `Bearer ${logoutData.accessToken}`, + }, + }); +} +// 사용자 비활성화 API +function inactiveUser(email) { + return instance.post('/user/inactive', { email }); +} + +async function getAllReview(userEmail, page = 0, size = 10) { try { - const response = await instance.get(`/reviews?page=${page}&size=${size}`); + const response = await instance.get(`/reviews`, { + params: { userEmail, page, size }, + }); if (response.status === 200) { const ReviewData = response.data; - console.log(ReviewData); + console.log("getAllReivew : " + JSON.stringify(ReviewData)); return ReviewData; } else { throw new Error('리뷰를 가져오는데 실패했습니다.'); @@ -122,9 +221,27 @@ async function getAllReview(page = 0, size = 20) { } } -async function getFollowingReview(id = 1, page = 0, size = 20) { +async function getFollowingReview(userEmail, page = 0, size = 10) { try { - const response = await instance.get(`/reviews/following?id=${id}&page=${page}&size=${size}`); + const response = await instance.get(`/reviews/following`, { + params: { userEmail, page, size }, + }); + if (response.status === 200) { + const ReviewData = response.data; + console.log(ReviewData); + return ReviewData; + } else { + throw new Error('리뷰를 가져오는데 실패했습니다.'); + } + } catch (error) { + console.error('오류:', error); + } +} +async function getMyReview(email, page = 0, size = 10) { + try { + const response = await instance.get(`/reviews/myreveiw`, { + params: { email, page, size }, + }); if (response.status === 200) { const ReviewData = response.data; console.log(ReviewData); @@ -148,12 +265,11 @@ async function imageUpload(imageFile) { 'Content-Type': 'multipart/form-data', }, }); - console.log('업로드 성공:', response); - alert('이미지 업로드 성공!'); + console.log('index.js function(imageUpload) : 업로드 성공 : ', response); return response; - } catch (error) { // try 블록 내에서 발생하는 예외를 캐치합니다. - console.error('업로드 실패:', error); - alert('이미지 업로드 실패'); + } catch (error) { + // try 블록 내에서 발생하는 예외를 캐치합니다. + console.error('index.js function(imageUpload) : 업로드 실패 : ', error); } } @@ -163,19 +279,13 @@ async function storeguide(storeId, router) { // 매장 ID를 서버에 전달하는 메소드입니다. // 여기서 HTTP 요청을 보내고 서버에서 작업을 처리할 수 있습니다. console.log('매장 ID:', storeId); - try { // Axios를 사용하여 GET 요청을 보냅니다. - const response = await axios.get( - `${process.env.VUE_APP_API_URL}store/${storeId}/guidemap`, - ); - + const response = await instance.get(`/store/${storeId}/guidemap`); // 서버로부터 받은 응답을 처리합니다. console.log('서버 응답:', response.data); - // 받은 데이터를 필요에 따라 처리합니다. // 이 부분에는 받은 데이터를 저장하거나 다른 작업을 수행할 수 있습니다. - // Vue Router를 사용하여 페이지를 이동합니다. router.push(`/store/${storeId}/guidemap`); } catch (error) { @@ -189,9 +299,13 @@ async function submitSurvey(surveyData) { for (let pair of surveyData.entries()) { console.log(pair[0] + ': ' + pair[1]); } - console.log("data 확인 완료"); + console.log('data 확인 완료'); // axios를 사용하여 서버에 데이터를 POST 방식으로 보냅니다. - const response = await instance.post('/reviews/submit', surveyData); + const response = await instance.post('/reviews/submit', surveyData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); console.log('설문 제출 성공:', response); alert('설문이 성공적으로 제출되었습니다!'); @@ -201,10 +315,46 @@ async function submitSurvey(surveyData) { } } +async function fetchRankUser(userEmail) { + try { + const response = await instance.get(`/reviews/rank`, { + params: { userEmail }, + }); + + if (response.status === 200) { + const RankData = response.data; + console.log(RankData); + return RankData; + } else { + throw new Error('랭킹 유저를 가져오는데 실패했습니다.'); + } + } catch (error) { + console.error('오류:', error); + } +} + +async function fetchAllUser(userEmail) { + try { + const response = await instance.get(`/reviews/allUser`, { + params: { userEmail }, + }); + + if (response.status === 200) { + const allUser = response.data; + console.log("fetchAllUser : " + allUser); + return allUser; + } else { + throw new Error('랭킹 유저를 가져오는데 실패했습니다.'); + } + } catch (error) { + console.error('오류:', error); + } +} + async function fetchReviewSelections(shopName) { try { const response = await instance.post('/review-selections/selection', { - shopName: shopName + shopName: shopName, }); console.log('Review Selections:', response.data); return response.data; @@ -214,32 +364,38 @@ async function fetchReviewSelections(shopName) { } //Mypage -function mypageData(userId) { - return instance.get(`/user/mypage/${userId}`); +function mypageData(email) { + return instance.get(`/user/mypage/${email}`); +} +function followData(email) { + return instance.get(`/follow/mycount/${email}`); +} +function myfollowingData(email) { + return instance.get(`/follow/from/${email}`); } -function followData(userId) { - return instance.get(`/follow/mycount/${userId}`); +function myfollowerData(email) { + return instance.get(`/follow/to/${email}`); } -function myfollowingData(userId) { - return instance.get(`/follow/from/${userId}`); +function deletemyfollowingData(email, id) { + return instance.delete(`/follow/${id}/deletefrom/${email}`); } -function myfollowerData(userId) { - return instance.get(`/follow/to/${userId}`); +function deletemyfollowerData(email, id) { + return instance.delete(`/follow/${id}/deleteto/${email}`); } -function deletemyfollowingData(userId, id) { - return instance.delete(`/follow/${id}/deletefrom/${userId}`); +function uploadProfileimg(email, formData, config) { + return instance.post(`/user/profile/upload/img/${email}`, formData, config); } -function deletemyfollowerData(userId, id) { - return instance.delete(`/follow/${id}/deleteto/${userId}`); +function readProfileimg(email) { + return instance.get(`/user/profile/get/img/${email}`); } -function uploadProfileimg(userId, formData, config) { - return instance.post(`/user/profile/upload/img/${userId}`, formData, config); +function deleteProfileimg(email) { + return instance.delete(`/user/profile/delete/img/${email}`); } -function readProfileimg(userId) { - return instance.get(`/user/profile/get/img/${userId}`); +function getEventList() { + return instance.get(`/board/list`); } -function deleteProfileimg(userId) { - return instance.delete(`/user/profile/delete/img/${userId}`); +function getEventDetail(boardId) { + return instance.get(`/board-image/content/${boardId}`); } export { @@ -255,6 +411,19 @@ export { rewardAdd, fetchStoreList, selectStore, + loginUser, + registerUser, + checkNicknameDuplicate, + checkEmailDuplicate, + sendAuthCode, + verifyAuthCode, + updatePassword, + loginWithOAuth2, + fetchUserData, + updateUserProfile, + getNewAccessToken, + logoutUser, + inactiveUser, storeguide, mypageData, followData, @@ -267,7 +436,15 @@ export { deleteProfileimg, getAllReview, imageUpload, + likeReview, submitSurvey, fetchReviewSelections, getFollowingReview, + getEventList, + getEventDetail, + fetchRankUser, + addFollowUser, + getMyReview, + fetchAllUser, + getStoreKeywords, }; diff --git a/src/components/LoadingSpinner.vue b/src/components/LoadingSpinner.vue new file mode 100644 index 0000000..0fefdab --- /dev/null +++ b/src/components/LoadingSpinner.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/PrivacyModal.vue b/src/components/PrivacyModal.vue new file mode 100644 index 0000000..7871643 --- /dev/null +++ b/src/components/PrivacyModal.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/components/TermsModal.vue b/src/components/TermsModal.vue new file mode 100644 index 0000000..eb45f62 --- /dev/null +++ b/src/components/TermsModal.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/src/components/review/ReviewButton.vue b/src/components/review/ReviewButton.vue index 2c51042..d80a3cd 100644 --- a/src/components/review/ReviewButton.vue +++ b/src/components/review/ReviewButton.vue @@ -1,28 +1,24 @@ - - - \ No newline at end of file + +.review_text { + display: none; + margin-left: 10px; + transition: all 0.5s; +} + +.review_button:hover .review_text { + display: block; +} + diff --git a/src/components/review/ReviewCard.js b/src/components/review/ReviewCard.js index 34483f3..fef00d9 100644 --- a/src/components/review/ReviewCard.js +++ b/src/components/review/ReviewCard.js @@ -1,19 +1,39 @@ export default { - storeReview: { - profilePic: require('@/img/review/profile_default_v2.png'), - username: "review-username", - userInfo: "유저의 리뷰 작성 갯수 ", - galleryImage: "https://ugc-images.catchtable.co.kr/rv/s3cSNdEyRL50KeKUYvOAJug/7e90e1e517c7438db576ac208d8d233b?detail750", - reviewContent: "review-content", - reviewdate: "review-date", - likecount : 5, - feedback: [ - { emoji: "https://ssl.pstatic.net/static/pup/emoji/face_savoring_food20220119222022.png", text: "review_feedback" }, - { emoji: "https://ssl.pstatic.net/static/pup/emoji/green_heart20220119222224.png", text: "review_feedback" }, - { emoji: "https://ssl.pstatic.net/static/pup/emoji/couch_and_lamp20220119222025.png", text: "review_feedback" }, - { emoji: "https://ssl.pstatic.net/static/pup/emoji/rice_ball20220119222026.png", text: "review_feedback" }, - { emoji: "https://ssl.pstatic.net/static/pup/emoji/light_bulb20220119222315.png", text: "review_feedback" } - ], - - } - }; \ No newline at end of file + storeReview: { + profilePic: require('@/img/review/profile_default_v2.png'), + username: 'review-username', + userInfo: '유저의 리뷰 작성 갯수 ', + galleryImage: + 'https://ugc-images.catchtable.co.kr/rv/s3cSNdEyRL50KeKUYvOAJug/7e90e1e517c7438db576ac208d8d233b?detail750', + reviewContent: 'review-content', + reviewdate: 'review-date', + likecount: 5, + feedback: [ + { + emoji: + 'https://ssl.pstatic.net/static/pup/emoji/face_savoring_food20220119222022.png', + text: 'review_feedback', + }, + { + emoji: + 'https://ssl.pstatic.net/static/pup/emoji/green_heart20220119222224.png', + text: 'review_feedback', + }, + { + emoji: + 'https://ssl.pstatic.net/static/pup/emoji/couch_and_lamp20220119222025.png', + text: 'review_feedback', + }, + { + emoji: + 'https://ssl.pstatic.net/static/pup/emoji/rice_ball20220119222026.png', + text: 'review_feedback', + }, + { + emoji: + 'https://ssl.pstatic.net/static/pup/emoji/light_bulb20220119222315.png', + text: 'review_feedback', + }, + ], + }, +}; diff --git a/src/components/review/ReviewCard.vue b/src/components/review/ReviewCard.vue index b936c67..d013dae 100644 --- a/src/components/review/ReviewCard.vue +++ b/src/components/review/ReviewCard.vue @@ -1,81 +1,415 @@ - - - \ No newline at end of file + +.timeline-post-footer .__post-meta > span::before { + content: none; +} + +.timeline-post-footer .__post-meta > span { + margin: 0; + padding: 0; +} + +.feedback-icons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.feedback { + display: flex; + align-items: center; + background-color: #f5f5f5; + border-radius: 8px; + padding: 4px 8px; +} + +.emoji-icon { + vertical-align: middle; + margin-right: 4px; +} + +.image-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 200px; + cursor: pointer; +} + +.image-container img { + max-width: 100%; + max-height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.swiper-button-prev, +.swiper-button-next { + color: #000; + width: 20px; + height: 20px; + z-index: 10; + cursor: pointer; +} + +.swiper-button-prev { + left: 10px; +} + +.swiper-button-next { + right: 10px; +} + +.btn-grey { + background-color: grey; + border-radius: 10px; +} + +.btn-rounded { + border-radius: 10px; +} + +.restaurant-info { + /* 회색 배경 */ + width: 230px; + /* border: 3px solid var(--mint-color); */ + display: flex; + /* 가로 방향으로 요소들을 나열 */ + align-items: center; + /* 세로 중앙 정렬 */ + height: fit-content; + /* 높이를 콘텐츠에 맞춤 */ +} + +.thumb { + width: 110px; + /* 이미지 크기 설정 */ + height: 60px; + /* 이미지 높이 자동 설정 */ + /* margin-right: 10px; */ + /* 이미지와 이름 사이 간격 */ + background-size: cover; + /* 이미지 크기를 컨테이너 전체를 채우도록 설정 */ +} + +.thumb img { + display: block; + /* 이미지를 블록 요소로 설정 */ +} + +.__info { + display: flex; + align-items: center; /* 세로 중앙 정렬 */ +} +.restaurant-info .__info { + margin-bottom: 0px; + /* border: 1px solid var(--mint-color); */ +} + +.__name { + cursor: pointer; /* 클릭 가능한 커서 표시 */ + /* margin-bottom: 10px; 아래쪽 여백 */ +} + +.__name:hover { + color: var(--primary-color); /* hover 시 글자색 변경 */ + text-decoration: underline; /* hover 시 밑줄 추가 */ +} + diff --git a/src/components/review/TimelinePost.vue b/src/components/review/TimelinePost.vue index 9dca91e..a4c21f4 100644 --- a/src/components/review/TimelinePost.vue +++ b/src/components/review/TimelinePost.vue @@ -1,12 +1,22 @@ @@ -58,7 +146,7 @@ export default { display: flex; /* Flexbox 레이아웃 사용 */ flex-direction: column; /* 자식 요소들을 세로로 정렬 */ align-items: center; /* 수평 방향 중앙 정렬 */ - height: 100vh; /* 뷰포트 높이를 전체 크기로 설정 */ + /* height: 100vh; 뷰포트 높이를 전체 크기로 설정 */ } .forgotpwform { margin-top: 40px; @@ -86,6 +174,11 @@ export default { border: 0; padding-left: 10px; } +#name:focus, +#email:focus { + border: 2px solid var(--mint-color) !important; + outline: none; +} #emailcheck { box-sizing: border-box; width: 100%; @@ -99,7 +192,7 @@ export default { cursor: pointer; /* 버튼 위에 마우스를 올렸을 때 커서 모양을 손가락 모양으로 변경 */ text-align: center; margin-top: 45px; - margin-bottom: 250px; + margin-bottom: 100px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); /* 테두리 대신 그림자를 추가합니다. */ } .loginback { @@ -116,4 +209,52 @@ export default { font-weight: bold; cursor: pointer; } +.warning { + color: #ff4057; +} +.validation-text { + margin-top: 5px; + font-size: 11px; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; +} +.notification { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 10px; + color: white; + background-color: var(--navy-color); + border-radius: 8px; + text-align: center; + width: 220px; + z-index: 2000; + opacity: 0.9; + animation: fadeIn 0.5s, fadeOut 0.5s 1.5s; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 0.9; + } +} +@keyframes fadeOut { + from { + opacity: 0.9; + } + to { + opacity: 0; + } +} +.fade-in { + animation: fadeIn 0.5s forwards; +} +.fade-out { + animation: fadeOut 0.5s forwards; +} diff --git a/src/views/login/LoginUser.vue b/src/views/login/LoginUser.vue index da53189..63793e4 100644 --- a/src/views/login/LoginUser.vue +++ b/src/views/login/LoginUser.vue @@ -16,6 +16,11 @@ placeholder="이메일을 입력해주세요" v-model="email" /> +

+ + 이메일 주소를 입력해주세요 + +

@@ -25,7 +30,6 @@ placeholder="비밀번호를 입력해주세요" v-model="password" /> -
@@ -33,22 +37,31 @@
- +
Or Login with
-
- +
+
-
- +
+
-
- +
+
+
+ {{ notification }} +
계정이 없으신가요? @@ -59,28 +72,98 @@ @@ -90,8 +173,9 @@ export default { display: flex; /* Flexbox 레이아웃 사용 */ flex-direction: column; /* 자식 요소들을 세로로 정렬 */ align-items: center; /* 수평 방향 중앙 정렬 */ - height: 100vh; /* 뷰포트 높이를 전체 크기로 설정 */ + /* height: 100vh; 뷰포트 높이를 전체 크기로 설정 */ padding-top: 60px; + padding-bottom: 0px; } .login-logo { width: 130px; /* 로고의 크기를 조정합니다. */ @@ -117,6 +201,9 @@ export default { position: relative; margin-bottom: 10px; } +/* .password-input { + margin-top: 25px; +} */ #email, #password { box-sizing: border-box; @@ -127,6 +214,11 @@ export default { border: 0; padding-left: 10px; } +#email:focus, +#password:focus { + border: 2px solid var(--mint-color) !important; + outline: none; +} .passwordeye { position: absolute; /* 이미지를 절대 위치로 설정 */ top: 50%; /* 상위 요소의 정중앙에서 시작 */ @@ -218,4 +310,52 @@ export default { font-weight: bold; cursor: pointer; } +.warning { + color: #ff4057; +} +.validation-text { + margin-top: 5px; + font-size: 11px; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; +} +.notification { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 10px; + color: white; + background-color: var(--navy-color); + border-radius: 8px; + text-align: center; + width: 220px; + z-index: 1000; + opacity: 0.9; + animation: fadeIn 0.5s, fadeOut 0.5s 1.5s; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 0.9; + } +} +@keyframes fadeOut { + from { + opacity: 0.9; + } + to { + opacity: 0; + } +} +.fade-in { + animation: fadeIn 0.5s forwards; +} +.fade-out { + animation: fadeOut 0.5s forwards; +} diff --git a/src/views/login/OAuth2Callback.vue b/src/views/login/OAuth2Callback.vue new file mode 100644 index 0000000..bc22760 --- /dev/null +++ b/src/views/login/OAuth2Callback.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/views/login/RegisterUser.vue b/src/views/login/RegisterUser.vue index 68c8bfe..fec7738 100644 --- a/src/views/login/RegisterUser.vue +++ b/src/views/login/RegisterUser.vue @@ -14,8 +14,14 @@ type="text" placeholder="이름을 입력해주세요" v-model="name" + @input="validateName" />
+

+ 이름을 확인해주세요 +

- +
+

+ 3자 이상 10자 이하로 문자(영어, 한국어), 숫자, 특수문자를 + 포함해주세요 + {{ + nicknameError + }} + {{ + nicknameSuccess + }} +

- +
+

+ + {{ emailError }} + +

- +
+

+ + {{ emailCodeMessage }} + +

@@ -60,8 +111,15 @@ type="password" placeholder="비밀번호를 입력해주세요" v-model="password" + @input="validatePassword" />
+

+ + 8자 이상, 숫자, 영문(대/소문자), 특수문자를 각각 하나 이상 + 포함해주세요 + +

@@ -70,8 +128,17 @@ type="password" placeholder="비밀번호를 한번 더 입력해주세요" v-model="passwordcheck" + @input="validatePasswordMatch" />
+

+ + {{ passwordMatchMessage }} + +

/ /
+

+ + {{ birthYearValidationMessage }} + + + {{ birthMonthValidationMessage }} + + + {{ birthDayValidationMessage }} + + + {{ birthValidationMessage }} + +

+

+ 핸드폰 번호를 확인해주세요 +

- - + -
- - -
- - - 내용보기 -
-
- - - 내용보기 -
-
- -
-
Or Register with
-
-
- +
+ + + 내용보기 +
+
+ + + 내용보기
-
- +
+
-
- +

+ 위에 모든 사항을 입력해주세요 +

+
Or Register with
+
+
+ +
+
+ +
+
+ +
-
+
이미 계정이 있으시다구요?
+ +
@@ -172,29 +286,320 @@ import passwordEye from '@/img/login/passwordeye.png'; import kakao from '@/img/login/kakaologo.png'; import google from '@/img/login/googlelogo.png'; import naver from '@/img/login/naverlogo.png'; +import TermsModal from '@/components/TermsModal.vue'; +import PrivacyModal from '@/components/PrivacyModal.vue'; +import { registerUser } from '@/api/index'; +import { + checkNicknameDuplicate, + checkEmailDuplicate, + sendAuthCode, + verifyAuthCode, +} from '@/api/index'; +import { + validateName, + validateNickname, + validateEmail, + validatePassword, + validatePhone, + validateYear, + validateMonth, + validateDay, + validateBirth, +} from '@/utils/validation'; export default { + components: { + TermsModal, + PrivacyModal, + }, data() { return { + name: '', + nickname: '', + nicknameError: '', + nicknameSuccess: '', + isNicknameDuplicate: false, + nicknameChecked: false, + email: '', + emailError: '', + isEmailDuplicate: false, + emailcode: '', + emailCodeMessage: '', + emailCodeVerified: false, + password: '', + passwordcheck: '', + passwordMatchMessage: '', passwordEye: passwordEye, birthYear: '', birthMonth: '', birthDay: '', + birthYearValid: true, + birthMonthValid: true, + birthDayValid: true, + birthYearValidationMessage: '', + birthMonthValidationMessage: '', + birthDayValidationMessage: '', + birthValidationMessage: '', + isBirthValid: true, + phone: '', + gender: 'choice', + agreecheck1: false, + agreecheck2: false, + formSubmitted: false, + formErrorMessage: '', + isTermsModalVisible: false, // 서비스 약관 모달 표시 여부 상태 추가 + isPrivacyModalVisible: false, // 개인정보 수집 및 이용 모달 표시 여부 상태 추가 kakao: kakao, google: google, naver: naver, }; }, + computed: { + isNameValid() { + return validateName(this.name); + }, + isNicknameValid() { + return validateNickname(this.nickname); + }, + isEmailValid() { + return validateEmail(this.email); + }, + isPasswordValid() { + return validatePassword(this.password); + }, + isPasswordMatch() { + return this.password === this.passwordcheck; + }, + isPhoneValid() { + return validatePhone(this.phone); + }, + isFormValid() { + return ( + this.isNameValid && + this.isEmailValid && + this.isNicknameValid && + this.isPasswordValid && + this.isPasswordMatch && + this.isBirthValid && + this.isPhoneValid && + this.gender !== 'choice' && + this.agreecheck1 && + this.agreecheck2 && + !this.nicknameError && + !this.emailError + ); + }, + }, + watch: { + name() { + this.validateName(); + }, + nickname() { + this.validateNickname(); + }, + email() { + this.validateEmailWatcher(); + }, + password() { + this.validatePassword(); + }, + passwordcheck() { + this.validatePasswordMatch(); + }, + birthYear() { + this.validateBirthYear(); + }, + birthMonth() { + this.validateBirthMonth(); + }, + birthDay() { + this.validateBirthDay(); + }, + phone() { + this.validatePhone(); + }, + }, methods: { - validateBirthInput(event) { - // 입력 검증 로직을 여기에 추가하세요. - // 예: 연도가 4자리인지, 월이 01-12 범위 내에 있는지, 일이 01-31 범위 내에 있는지 등 - const yearInput = event.target.value; // 현재 입력된 연도 - this.birthYearValid = yearInput.length === 4; // 연도가 4자리인지 확인 + validateName() { + return validateName(this.name); + }, + validateNickname() { + this.nicknameChecked = false; + if (!validateNickname(this.nickname)) { + this.nicknameError = + '3자 이상 10자 이하로 문자(영어, 한국어), 숫자, 특수문자를 포함해주세요'; + } else { + this.nicknameError = ''; + } + }, + async validateAndCheckNickname() { + this.validateNickname(); + if (this.nicknameError) { + this.nicknameSuccess = ''; + this.isNicknameDuplicate = false; + return; + } + this.nicknameChecked = true; + this.nicknameError = ''; + this.nicknameSuccess = ''; + try { + const response = await checkNicknameDuplicate(this.nickname); + if (response.data) { + this.nicknameError = '중복된 닉네임입니다'; + this.isNicknameDuplicate = true; + } else { + this.nicknameSuccess = '사용 가능한 닉네임입니다'; + this.isNicknameDuplicate = false; + } + } catch (error) { + this.nicknameError = '닉네임 중복 체크 중 오류가 발생했습니다'; + this.isNicknameDuplicate = true; + } + }, + validateEmailWatcher() { + return validateEmail(this.email); + }, + async validateAndCheckEmail() { + if (!validateEmail(this.email)) { + this.emailError = '이메일 주소를 확인해주세요'; + this.isEmailDuplicate = false; + return; + } + this.emailError = ''; + try { + const response = await checkEmailDuplicate(this.email); + if (response.data) { + this.emailError = '중복된 이메일입니다'; + this.isEmailDuplicate = true; + } else { + this.emailError = '인증 코드가 전송되었습니다'; + this.isEmailDuplicate = false; + await sendAuthCode(this.email); + } + } catch (error) { + this.emailError = '이메일 중복 체크 중 오류가 발생했습니다'; + this.isEmailDuplicate = true; + } + }, + async verifyEmailCode() { + try { + const response = await verifyAuthCode(this.email, this.emailcode); + if (response.data) { + this.emailCodeMessage = '이메일 인증이 완료되었습니다'; + this.emailCodeVerified = true; + this.emailError = ''; + } else { + this.emailCodeMessage = '인증 코드가 올바르지 않습니다'; + this.emailCodeVerified = false; + } + } catch (error) { + this.emailCodeMessage = '이메일 인증 코드 검증 중 오류가 발생했습니다'; + this.emailCodeVerified = false; + } + }, + validatePassword() { + return validatePassword(this.password); + }, + validatePasswordMatch() { + if (this.password === this.passwordcheck) { + this.passwordMatchMessage = '비밀번호가 일치합니다'; + } else { + this.passwordMatchMessage = '비밀번호가 일치하지 않습니다'; + } + return this.password === this.passwordcheck; + }, + validateBirthYear() { + const isValid = validateYear(this.birthYear); + this.birthYearValid = isValid; + this.updateBirthValidationMessage(); + return isValid; + }, + validateBirthMonth() { + const isValid = validateMonth(this.birthMonth); + this.birthMonthValid = isValid; + this.updateBirthValidationMessage(); + return isValid; + }, + validateBirthDay() { + const isValid = validateDay(this.birthDay); + this.birthDayValid = isValid; + this.updateBirthValidationMessage(); + return isValid; + }, + updateBirthValidationMessage() { + const isValid = validateBirth( + this.birthYear, + this.birthMonth, + this.birthDay, + ); + this.birthValidationMessage = isValid ? '' : '유효한 날짜를 입력해주세요'; + this.isBirthValid = isValid; + }, + validatePhone() { + return validatePhone(this.phone); }, gotoLogin() { this.$router.push('/login'); }, + openTermsModal() { + this.isTermsModalVisible = true; + }, + closeTermsModal() { + this.isTermsModalVisible = false; + }, + openPrivacyModal() { + this.isPrivacyModalVisible = true; + }, + closePrivacyModal() { + this.isPrivacyModalVisible = false; + }, + async submitForm() { + this.formSubmitted = true; + console.log(this.isFormValid); + console.log(this.isNameValid); + console.log(this.isEmailValid); + console.log(this.isNicknameValid); + console.log(this.isPasswordValid); + console.log(this.isPasswordMatch); + console.log(this.isBirthValid); + console.log(this.isPhoneValid); + console.log(this.isNicknameValid); + console.log(this.gender); + console.log(this.agreecheck1); + console.log(this.agreecheck2); + console.log(this.nicknameError); + console.log(this.emailError); + if (this.isFormValid) { + try { + const userData = { + name: this.name, + nickname: this.nickname, + email: this.email, + password: this.password, + birth: `${this.birthYear}-${this.birthMonth}-${this.birthDay}`, + phone: this.phone, + gender: this.gender.toUpperCase(), + provider_type: 'LOCAL', + agreecheck1: this.agreecheck1, + agreecheck2: this.agreecheck2, + }; + + const response = await registerUser(userData); + if (response.status === 200) { + console.log('회원가입 성공'); + this.$router.push('/main'); // 회원가입 성공 후 메인 페이지로 이동 + } else { + console.log('회원가입 실패'); + this.formErrorMessage = '회원가입에 실패했습니다.'; + } + } catch (error) { + console.log('API 호출 오류:', error); + this.formErrorMessage = '회원가입 중 오류가 발생했습니다.'; + } + } else { + this.formErrorMessage = '위에 모든 사항을 입력해주세요'; + } + }, }, }; @@ -205,12 +610,16 @@ export default { flex-direction: column; /* 자식 요소들을 세로로 정렬 */ align-items: center; /* 수평 방향 중앙 정렬 */ height: 100vh; /* 뷰포트 높이를 전체 크기로 설정 */ + padding-bottom: 0px; } .registerform { text-align: center; position: relative; width: 350px; } +.input.my-input:focus { + border-color: var(--mint-color); /* 원하는 색상으로 변경 */ +} .nameinput, .passwordinput, .passwordcheckinput, @@ -303,6 +712,20 @@ export default { border: 0; padding-left: 10px; } +#name:focus, +#nickname:focus, +#email:focus, +#emailcode:focus, +#password:focus, +#passwordcheck:focus, +#birthYear:focus, +#birthMonth:focus, +#birthDay:focus, +#phone:focus, +#gender:focus { + border: 2px solid var(--mint-color) !important; + outline: none; +} #registerclear { box-sizing: border-box; width: 100%; @@ -394,4 +817,17 @@ export default { font-weight: bold; cursor: pointer; } +.success { + color: var(--mint-color); +} +.warning { + color: #ff4057; +} +.validation-text { + margin-top: 5px; + font-size: 12.5px; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; +} diff --git a/src/views/mypage/MypageCoupon.vue b/src/views/mypage/MypageCoupon.vue index e0d02d3..d84f455 100644 --- a/src/views/mypage/MypageCoupon.vue +++ b/src/views/mypage/MypageCoupon.vue @@ -1,6 +1,5 @@ @@ -54,7 +86,7 @@ export default { text-align: left; } -.registerform { +.inactiveform { display: flex; flex-direction: column; align-content: center; diff --git a/src/views/review/DateModal.vue b/src/views/review/DateModal.vue index 8da7567..9fef2db 100644 --- a/src/views/review/DateModal.vue +++ b/src/views/review/DateModal.vue @@ -1,9 +1,10 @@ @@ -13,26 +14,26 @@ export default { props: ['isVisible'], data() { return { - selectedDate: '' - } + selectedDate: '', + }; }, methods: { - closeModal() { - this.$emit('update:isVisible', false); + closeModal() { + this.$emit('update:isVisible', false); + }, + submitDate() { + this.$emit('dateSelected', this.selectedDate); // 시간 전달 + this.closeModal(); + }, }, - submitDate() { - this.$emit('dateSelected', this.selectedDate); // 시간 전달 - this.closeModal(); - } -} -} +}; \ No newline at end of file + diff --git a/src/views/review/OCR.vue b/src/views/review/OCR.vue index 0a65744..98f9258 100644 --- a/src/views/review/OCR.vue +++ b/src/views/review/OCR.vue @@ -1,4 +1,7 @@ - - +.btn-grey { + background-color: grey; + border-radius: 10px; +} +.btn-rounded { + border-radius: 10px; +} +.container { + display: flex; + justify-content: center; + align-items: center; + height: 50vh; + text-align: center; +} +.message { + font-size: 1.5em; +} + \ No newline at end of file diff --git a/src/views/review/ReviewRecommended.vue b/src/views/review/ReviewRecommended.vue index 7e1ad0c..032f27d 100644 --- a/src/views/review/ReviewRecommended.vue +++ b/src/views/review/ReviewRecommended.vue @@ -10,18 +10,24 @@
-
로딩 중...
-
데이터가 없습니다.
- + +
+
로딩 중...
+
+
+ +
+
diff --git a/src/views/review/ReviewUser.vue b/src/views/review/ReviewUser.vue index 2c7678b..aa62b71 100644 --- a/src/views/review/ReviewUser.vue +++ b/src/views/review/ReviewUser.vue @@ -1,146 +1,194 @@ \ No newline at end of file +@import '@/css/review/review.css'; + diff --git a/src/views/review/ReviewWrite.vue b/src/views/review/ReviewWrite.vue index c26abb1..60ef7cb 100644 --- a/src/views/review/ReviewWrite.vue +++ b/src/views/review/ReviewWrite.vue @@ -1,11 +1,16 @@ + \ No newline at end of file + +.notification { + position: absolute; + top: 160px; + left: 120px; + margin: auto; + padding: 10px; + color: white; + background-color: var(--navy-color); + border-radius: 8px; + text-align: center; + width: 220px; + z-index: 1000; + /* z-index 추가 */ + opacity: 0.9; + /* 불투명도 추가 */ + animation: fadeIn 0.5s, fadeOut 0.5s 1.5s; + /* 애니메이션 추가 */ +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 0.9; + } +} + +@keyframes fadeOut { + from { + opacity: 0.9; + } + + to { + opacity: 0; + } +} + +.fade-in { + animation: fadeIn 0.5s forwards; +} + +.fade-out { + animation: fadeOut 0.5s forwards; +} + diff --git a/src/views/review/TimeModal.vue b/src/views/review/TimeModal.vue index 2c28ae7..b5a7ca0 100644 --- a/src/views/review/TimeModal.vue +++ b/src/views/review/TimeModal.vue @@ -1,9 +1,10 @@ @@ -13,26 +14,26 @@ export default { props: ['isVisible'], data() { return { - selectedTime: '' - } + selectedTime: '', + }; }, methods: { - closeModal() { - this.$emit('update:isVisible', false); + closeModal() { + this.$emit('update:isVisible', false); + }, + submitTime() { + this.$emit('timeSelected', this.selectedTime); // 시간 전달 + this.closeModal(); + }, }, - submitTime() { - this.$emit('timeSelected', this.selectedTime); // 시간 전달 - this.closeModal(); - } -} -} +}; - diff --git a/src/views/store/MallMap.vue b/src/views/store/MallMap.vue index ec35743..96aeb7f 100644 --- a/src/views/store/MallMap.vue +++ b/src/views/store/MallMap.vue @@ -1,6 +1,5 @@