├── .browserslistrc ├── src ├── utils │ ├── bus.js │ ├── libs.js │ ├── sentry.js │ ├── dragger │ │ ├── dragula.css │ │ └── index.js │ ├── meta.js │ ├── social │ │ ├── index.js │ │ ├── Kakao.js │ │ ├── Google.js │ │ └── Facebook.js │ ├── socket.js │ ├── fontAwesomeIcon.js │ ├── webStorage.js │ └── validation.js ├── assets │ ├── icon.png │ ├── logo.png │ └── user │ │ ├── back │ │ ├── 1.png │ │ └── 2.png │ │ └── logo │ │ ├── github.png │ │ ├── google.png │ │ ├── kakao.png │ │ └── facebook.png ├── store │ ├── getters.js │ ├── index.js │ ├── state.js │ ├── mutations.js │ └── actions.js ├── features │ ├── directive.js │ ├── plugin.js │ └── filter.js ├── api │ ├── email.js │ ├── like.js │ ├── common │ │ └── interceptors.js │ ├── list.js │ ├── hashtag.js │ ├── upload.js │ ├── checklistItem.js │ ├── index.js │ ├── pushMessage.js │ ├── comment.js │ ├── auth.js │ ├── card.js │ ├── checklist.js │ ├── user.js │ └── board.js ├── main.js ├── components │ ├── common │ │ ├── CommonFooter.vue │ │ ├── header │ │ │ ├── message │ │ │ │ ├── HeaderMessageModal.vue │ │ │ │ ├── HeaderMessage.vue │ │ │ │ └── HeaderMessageModalConfirm.vue │ │ │ └── CommonHeaderMenu.vue │ │ ├── LoadingSpinner.vue │ │ ├── MiniModal.vue │ │ └── AlertNotification.vue │ ├── profile │ │ ├── ProfileTabItem.vue │ │ ├── PasswordChange.vue │ │ └── AboutUserSignout.vue │ ├── card │ │ ├── cardModal │ │ │ ├── subModal │ │ │ │ ├── SubModalDeleteCard.vue │ │ │ │ ├── SubModalChecklist.vue │ │ │ │ ├── SubModalLocation.vue │ │ │ │ ├── SubModalAttachment.vue │ │ │ │ ├── SubModalDueDate.vue │ │ │ │ ├── SubModalLabels.vue │ │ │ │ └── SubModal.vue │ │ │ ├── mainModal │ │ │ │ ├── CardLabels.vue │ │ │ │ ├── attachment │ │ │ │ │ ├── CardAttachment.vue │ │ │ │ │ └── CardAttachmentList.vue │ │ │ │ ├── checklists │ │ │ │ │ ├── CardChecklist.vue │ │ │ │ │ └── CardChecklistWrapItem.vue │ │ │ │ ├── location │ │ │ │ │ ├── CardLocationMapPin.vue │ │ │ │ │ ├── CardLocationMapBase.vue │ │ │ │ │ └── CardLocation.vue │ │ │ │ ├── CardDueDate.vue │ │ │ │ └── CardDescription.vue │ │ │ └── CardModalBase.vue │ │ ├── AddCard.vue │ │ └── CardItem.vue │ ├── board │ │ ├── boardHeader │ │ │ ├── BoardHeaderDisclosureStatus.vue │ │ │ ├── boardMenu │ │ │ │ └── BoardMenuColorPicker.vue │ │ │ ├── boardInvite │ │ │ │ ├── InviteModalList.vue │ │ │ │ └── InviteModal.vue │ │ │ ├── BoardHeaderProfileImage.vue │ │ │ └── BoardHeaderHashtagModal.vue │ │ └── addBoard │ │ │ └── AddBoardModalBase.vue │ ├── list │ │ └── AddList.vue │ ├── mainPage │ │ ├── MainTab.vue │ │ └── section │ │ │ ├── InvitedSection.vue │ │ │ └── PersonalSection.vue │ └── auth │ │ └── FindPassword.vue ├── routes │ ├── navigationGuard.js │ └── index.js ├── App.vue ├── views │ ├── NotFoundPage.vue │ ├── MainPage.vue │ └── AuthPage.vue └── css │ └── common.scss ├── jest.config.js ├── .env.production.enc ├── babel.config.js ├── public ├── icon-cutout.png └── index.html ├── .gitignore ├── jsconfig.json ├── tests └── unit │ └── example.spec.js ├── vue.config.js ├── .travis.yml ├── package.json └── .eslintrc.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /src/utils/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | export default new Vue(); 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest" 3 | }; 4 | -------------------------------------------------------------------------------- /.env.production.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/.env.production.enc -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/icon-cutout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/public/icon-cutout.png -------------------------------------------------------------------------------- /src/assets/user/back/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/back/1.png -------------------------------------------------------------------------------- /src/assets/user/back/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/back/2.png -------------------------------------------------------------------------------- /src/assets/user/logo/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/logo/github.png -------------------------------------------------------------------------------- /src/assets/user/logo/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/logo/google.png -------------------------------------------------------------------------------- /src/assets/user/logo/kakao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/logo/kakao.png -------------------------------------------------------------------------------- /src/assets/user/logo/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pozafly/tripllo_vue/HEAD/src/assets/user/logo/facebook.png -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isAuth(state) { 3 | return !!state.token; 4 | }, 5 | }; 6 | 7 | export default getters; 8 | -------------------------------------------------------------------------------- /src/features/directive.js: -------------------------------------------------------------------------------- 1 | const directives = Vue => { 2 | // v-focus 3 | Vue.directive('focus', { 4 | inserted(el) { 5 | el.focus(); 6 | }, 7 | }); 8 | 9 | // 사용자 디렉티브 계속 추가 가능 10 | }; 11 | 12 | export { directives }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env 8 | .env.production 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/utils/libs.js: -------------------------------------------------------------------------------- 1 | // null 체크 2 | const isEmpty = value => { 3 | if ( 4 | value === '' || 5 | value === null || 6 | value === undefined || 7 | value === 'null' || 8 | value === '[]' || 9 | value.length === 0 10 | ) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | }; 16 | 17 | export { isEmpty }; 18 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", // 새로운 폴더로 들어와서 작업 4 | "paths": { 5 | "~/*": [ // ~ 6 | "./*" 7 | ], 8 | "@/*": [ // @는 src 라는 뜻이 된다. 9 | "./src/*" 10 | ], 11 | } 12 | }, 13 | "exclude": [ // 컴파일 되지 않는 폴더들 14 | "node_modules", 15 | "dist" 16 | ] 17 | } -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import HelloWorld from "@/components/HelloWorld.vue"; 3 | 4 | describe("HelloWorld.vue", () => { 5 | it("renders props.msg when passed", () => { 6 | const msg = "new message"; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }); 10 | expect(wrapper.text()).toMatch(msg); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import actions from '@/store/actions'; 4 | import getters from '@/store/getters'; 5 | import state from '@/store/state'; 6 | import mutations from '@/store/mutations'; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | strict: process.env.NODE_ENV === 'development', 12 | state, 13 | getters, 14 | mutations, 15 | actions, 16 | }); 17 | -------------------------------------------------------------------------------- /src/api/email.js: -------------------------------------------------------------------------------- 1 | import { email } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} Email 5 | * @property {string} userId - 이메일을 보낼 대상 ID 6 | * @property {string} userEmail - 이메일 주소 7 | */ 8 | 9 | /** 10 | * 비밀번호를 바꾸기 위해 Email 전송 11 | * @param {Email} emailInfo 12 | * @returns {Promise} statusCode - 상태코드(성공여부) 13 | */ 14 | const sendEmailAPI = emailInfo => email.post(`/`, emailInfo); 15 | 16 | export { sendEmailAPI }; 17 | -------------------------------------------------------------------------------- /src/utils/sentry.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import * as Sentry from '@sentry/vue'; 3 | import { Integrations } from '@sentry/tracing'; 4 | 5 | export default Sentry.init({ 6 | Vue, 7 | dsn: process.env.VUE_APP_SENTRY_DSN, 8 | integrations: [new Integrations.BrowserTracing()], 9 | attachProps: true, 10 | logErrors: false, 11 | environment: process.env.NODE_ENV, 12 | enabled: process.env.NODE_ENV !== 'development', 13 | 14 | tracesSampleRate: 1.0, 15 | }); 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from '@/routes'; 4 | import store from '@/store'; 5 | import { plugins } from '@/features/plugin'; 6 | import { directives } from '@/features/directive'; 7 | import { filters } from '@/features/filter'; 8 | import '@/utils/fontAwesomeIcon.js'; 9 | import '@/utils/sentry'; 10 | import Meta from 'vue-meta'; 11 | 12 | Vue.config.productionTip = false; 13 | 14 | Vue.use(plugins); 15 | Vue.use(directives); 16 | Vue.use(filters); 17 | Vue.use(Meta); 18 | 19 | new Vue({ 20 | render: h => h(App), 21 | router, 22 | store, 23 | }).$mount('#app'); 24 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | import { 2 | getUserFromLocalStorage, 3 | getSessionStorage, 4 | getTokenFromLocalStorage, 5 | } from '@/utils/webStorage'; 6 | 7 | const state = { 8 | token: getTokenFromLocalStorage() || '', 9 | user: getUserFromLocalStorage() || '', 10 | board: getSessionStorage('board') || {}, 11 | card: getSessionStorage('card') || {}, 12 | bgColor: getSessionStorage('bgColor') || '', 13 | // personalBoard, recentBoard는 서로 sync가 맞아야 하기때문에 14 | // store에 있어야한다고 판단. 15 | personalBoard: [], 16 | recentBoard: [], 17 | pushMessage: '', // pushMessage는 3depths & socket.js까지 사용하므로 store에 있는게 좋겠다. 18 | file: [], // 4depths 19 | }; 20 | 21 | export default state; 22 | -------------------------------------------------------------------------------- /src/utils/dragger/dragula.css: -------------------------------------------------------------------------------- 1 | .gu-mirror { 2 | position: fixed !important; 3 | margin: 0 !important; 4 | z-index: 9999 !important; 5 | opacity: 0.8; 6 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; 7 | filter: alpha(opacity=80); 8 | transform: rotate( 5deg ); 9 | } 10 | .gu-hide { 11 | display: none !important; 12 | } 13 | .gu-unselectable { 14 | -webkit-user-select: none !important; 15 | -moz-user-select: none !important; 16 | -ms-user-select: none !important; 17 | user-select: none !important; 18 | } 19 | .gu-transit { 20 | opacity: 0.2; 21 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; 22 | filter: alpha(opacity=20); 23 | } 24 | -------------------------------------------------------------------------------- /src/api/like.js: -------------------------------------------------------------------------------- 1 | import { boardHasLike } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} LikeInfo 5 | * @property {number} boardId - Board ID 6 | * @property {number} likeCount - 좋아요 갯수 7 | */ 8 | 9 | /** 10 | * 좋아요 count + 1 11 | * @param {LikeInfo} likeInfo 12 | * @returns {Promise} statusCode - 상태코드 13 | */ 14 | const createLikeAPI = likeInfo => boardHasLike.post('/', likeInfo); 15 | 16 | /** 17 | * 좋아요 count - 1 18 | * @param {LikeInfo} likeInfo 19 | * @returns {Promise} statusCode - 상태코드 20 | */ 21 | const deleteLikeAPI = ({ boardId, likeCount }) => 22 | // SpringBoot의 DeleteMapping에서 @PathVariable 때문에 payload(객체 전달) 불가 23 | boardHasLike.delete(`/${boardId}/${likeCount}`); 24 | 25 | export { createLikeAPI, deleteLikeAPI }; 26 | -------------------------------------------------------------------------------- /src/utils/meta.js: -------------------------------------------------------------------------------- 1 | export const defualtMeta = [ 2 | { 3 | vmid: 'description', 4 | name: 'description', 5 | content: 6 | 'Tripllo를 통해 체계적으로 계획을 공유하고 다양한 컨텐츠를 만나보세요', 7 | }, 8 | { 9 | vmid: 'keywords', 10 | name: 'keywords', 11 | content: 'Tripllo,계획공유,칸반보드', 12 | }, 13 | { 14 | vmid: 'author', 15 | name: 'author', 16 | content: 'pozafly', 17 | }, 18 | { 19 | vmid: 'og:title', 20 | property: 'og:title', 21 | content: 'Tripllo', 22 | }, 23 | { 24 | vmid: 'og:description', 25 | property: 'og:description', 26 | content: 27 | 'Tripllo를 통해 체계적으로 계획을 공유하고 다양한 컨텐츠를 만나보세요', 28 | }, 29 | { 30 | vmid: 'og:url', 31 | property: 'og:url', 32 | content: 'https://tripllo.tech/', 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/components/common/CommonFooter.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /src/api/common/interceptors.js: -------------------------------------------------------------------------------- 1 | // axios의 interceptor 설정 2 | import store from '@/store'; 3 | 4 | export const setInterceptors = instance => { 5 | // request 6 | instance.interceptors.request.use( 7 | config => { 8 | config.headers.Authorization = store.state.token; 9 | return config; 10 | }, 11 | error => { 12 | return Promise.reject(error); 13 | }, 14 | ); 15 | 16 | // response 17 | instance.interceptors.response.use( 18 | response => { 19 | return response; 20 | }, 21 | error => { 22 | const errorCode = error.response.status; 23 | if (errorCode === 401 || errorCode === 403) { 24 | alert('권한이 없습니다.'); 25 | } else if (errorCode === 400) { 26 | alert('잘못된 요청입니다.'); 27 | } 28 | return Promise.reject(error); 29 | }, 30 | ); 31 | 32 | return instance; 33 | }; 34 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const SentryWebpackPlugin = require('@sentry/webpack-plugin'); 2 | const sentryPlugin = 3 | process.env.NODE_ENV !== 'development' 4 | ? [ 5 | new SentryWebpackPlugin({ 6 | // sentry-cli configuration 7 | authToken: process.env.VUE_APP_SENTRY_AUTH_TOKEN, 8 | org: 'tripllo_vue', 9 | project: 'tripllo_vue', 10 | // webpack specific configuration 11 | include: './dist', 12 | ignore: ['node_modules', 'vue.config.js'], 13 | }), 14 | ] 15 | : []; 16 | 17 | module.exports = { 18 | devServer: { 19 | proxy: { 20 | '/api': { 21 | target: 'http://localhost:3000', 22 | }, 23 | '/websocket': { 24 | target: 'http://localhost:3000', 25 | }, 26 | }, 27 | overlay: false, 28 | }, 29 | // other configuration 30 | configureWebpack: { 31 | plugins: sentryPlugin, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/features/plugin.js: -------------------------------------------------------------------------------- 1 | import $_Google from '@/utils/social/Google'; 2 | import $_Kakao from '@/utils/social/Kakao'; 3 | import $_Facebook from '@/utils/social/Facebook'; 4 | 5 | import $_KProgress from 'k-progress'; 6 | import $_DatePicker from 'v-calendar/lib/components/date-picker.umd'; 7 | import $_MiniModal from '@/components/common/MiniModal.vue'; 8 | import $_Notifications from 'vue-notification'; 9 | import $_vClickOutside from 'v-click-outside'; 10 | import $_InfiniteLoading from 'vue-infinite-loading'; 11 | import $_LoadScript from 'vue-plugin-load-script'; 12 | 13 | const plugins = Vue => { 14 | Vue.prototype.$_Kakao = $_Kakao; 15 | Vue.prototype.$_Google = $_Google; 16 | Vue.prototype.$_Facebook = $_Facebook; 17 | Vue.component('KProgress', $_KProgress); 18 | Vue.component('DatePicker', $_DatePicker); 19 | Vue.component('MiniModal', $_MiniModal); 20 | Vue.use($_LoadScript); 21 | Vue.use($_Notifications); 22 | Vue.use($_vClickOutside); 23 | Vue.use($_InfiniteLoading); 24 | }; 25 | 26 | export { plugins }; 27 | -------------------------------------------------------------------------------- /src/routes/navigationGuard.js: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { getUserFromLocalStorage } from '@/utils/webStorage'; 3 | import { isEmpty } from '@/utils/libs'; 4 | 5 | const firstAccess = (to, from, next) => { 6 | if (isEmpty(getUserFromLocalStorage())) { 7 | next('/intro'); 8 | return; 9 | } else { 10 | next('/main'); 11 | return; 12 | } 13 | }; 14 | 15 | const requireAuth = (to, from, next) => { 16 | const loginPath = '/auth'; 17 | if (store.getters.isAuth) { 18 | next(); 19 | return; 20 | } else { 21 | alert('로그인 되어있지 않습니다.'); 22 | next(loginPath); 23 | return; 24 | } 25 | }; 26 | 27 | const intoBoard = async (to, from, next) => { 28 | try { 29 | await store.dispatch('READ_BOARD_DETAIL', to.params.boardId); 30 | // 인증 부분으로 매개변수 넘겨줌. 31 | requireAuth(to, from, next); 32 | } catch (error) { 33 | console.log(error); 34 | if (error.response.status === 404) { 35 | alert('해당 Board의 정보가 없습니다.'); 36 | } 37 | next('/main'); 38 | return; 39 | } 40 | }; 41 | 42 | export { firstAccess, requireAuth, intoBoard }; 43 | -------------------------------------------------------------------------------- /src/api/list.js: -------------------------------------------------------------------------------- 1 | import { list } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} CreateListInfo 5 | * @property {number} boardId - list가 어느 board에 속해있는지 구분 6 | * @property {number} pos - Dragula 포지션 값 7 | * @property {string} title - 제목 8 | */ 9 | 10 | /** 11 | * @typedef {Object} UpdateListInfo 12 | * @property {number} id - list ID 13 | * @property {number} pos - Dragula 포지션 값 14 | * @property {string} title - 제목 15 | */ 16 | 17 | /** 18 | * 리스트 생성 19 | * @param {CreateListInfo} createListInfo 20 | * @returns {Promise} statusCode - 상태코드 21 | */ 22 | const createListAPI = createListInfo => list.post('/', createListInfo); 23 | 24 | /** 25 | * 리스트 수정 26 | * @param {CreateListInfo} updateListInfo 27 | * @returns {Promise} statusCode - 상태코드 28 | */ 29 | const updateListAPI = (id, updateListInfo) => 30 | list.put(`/${id}`, updateListInfo); 31 | 32 | /** 33 | * 리스트 삭제 34 | * @param {number} id - 리스트 ID 35 | * @returns {Promise} statusCode - 상태코드 36 | */ 37 | const deleteListAPI = id => list.delete(`/${id}`); 38 | 39 | export { createListAPI, updateListAPI, deleteListAPI }; 40 | -------------------------------------------------------------------------------- /src/api/hashtag.js: -------------------------------------------------------------------------------- 1 | import { hashtag } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} HashtagBoardInfo 5 | * @property {string} hashtagName - 해시태그 이름 6 | * @property {string} lastLikeCount - 무한 로딩에 필요한 마지막 좋아요 갯수(조회 조건에 필요 - 커서 기반 페이지네이션) 7 | * @property {string} lastCreatedAt - 무한 로딩에 필요한 마지막 생성날짜(조회 조건에 필요) 8 | */ 9 | 10 | /** 11 | * @typedef {Object} HashRanking 12 | * @property {number} count - 해시태그가 등록된 갯수 13 | * @property {number} name - 해시태그 이름 14 | */ 15 | 16 | /** 17 | * public section에서, 해시태그로 검색했을 시 해시태그를 가진 Board 목록을 조회 18 | * @param {HashtagBoardInfo} hashtagBoardInfo 19 | * @returns {Promise} 20 | */ 21 | const readBoardByHashtagAPI = ({ hashtagName, lastLikeCount, lastCreatedAt }) => 22 | // hashtag.get('/', { hashtagName, lastLikeCount, lastCreatedAt }); 23 | // 이게 안먹힌다 ㅜㅜ 왜지.. 24 | hashtag.get( 25 | `/?hashtagName=${hashtagName}&lastLikeCount=${lastLikeCount}&lastCreatedAt=${lastCreatedAt}`, 26 | ); 27 | 28 | /** 29 | * public section에서, 해시태그 랭킹 표시 30 | * @returns {Promise} 31 | */ 32 | const readRankingByLikeCountAPI = () => hashtag.get('/orderByCount'); 33 | 34 | export { readBoardByHashtagAPI, readRankingByLikeCountAPI }; 35 | -------------------------------------------------------------------------------- /src/components/common/header/message/HeaderMessageModal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /src/components/profile/ProfileTabItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 60 | -------------------------------------------------------------------------------- /src/api/upload.js: -------------------------------------------------------------------------------- 1 | import { upload } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} File 5 | * @property {string} id - file ID 6 | * @property {string} extension - 확장자 명 7 | * @property {string} fileName - file 명 8 | * @property {string} link - 파일 다운로드 경로 9 | * @property {number} cardId - file이 어느 card에 속해있는지 구분 10 | * @property {string} createdAt - 생성날짜 11 | * @property {string} createdBy - 생성자 12 | */ 13 | 14 | /** 15 | * 파일 목록 조회 16 | * @param {number} cardId - card ID 17 | * @returns {Promise} 18 | */ 19 | const readFileAPI = cardId => upload.get(`/${cardId}`); 20 | 21 | /** 22 | * 파일 업로드 23 | * @param {multipart} formData - 파일 24 | * @returns {Promise} statusCode - 상태코드 25 | */ 26 | const uploadFileAPI = formData => upload.post(`/`, formData); 27 | 28 | /** 29 | * 프로필 사진 업로드 30 | * @param {multipart} imageData - 파일/이미지 31 | * @returns {Promise} statusCode - 상태코드 32 | */ 33 | const uploadImageAPI = imageData => upload.post('/image', imageData); 34 | 35 | /** 36 | * 파일 삭제 37 | * @param {number} fileId - 파일 ID 38 | * @returns {Promise} statusCode - 상태코드 39 | */ 40 | const deleteFileAPI = fileId => upload.delete(`/${fileId}`); 41 | 42 | export { readFileAPI, uploadFileAPI, uploadImageAPI, deleteFileAPI }; 43 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalDeleteCard.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /src/components/common/LoadingSpinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 67 | -------------------------------------------------------------------------------- /src/utils/social/index.js: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import router from '@/routes'; 3 | import { socialLoginAPI, validIdAPI, signupAPI } from '@/api/auth'; 4 | 5 | const socialLogin = async (req, isSignup) => { 6 | try { 7 | const { data } = await socialLoginAPI(req.id); 8 | store.commit('setUserToken', data.data.token); 9 | store.commit('setUser', data.data); 10 | 11 | if (isSignup === 'afterSignup') { 12 | alert('회원가입 완료! 메인 페이지로 이동합니다.'); 13 | } 14 | 15 | router.push('/main'); 16 | } catch (error) { 17 | console.log(error); 18 | 19 | const { data } = await validIdAPI(req.id); 20 | if (data.status === 'OK') { 21 | const confirmYn = confirm( 22 | '아직 가입되지 않은 회원입니다. \n회원가입 화면으로 이동하시겠습니까?', 23 | ); 24 | if (confirmYn) { 25 | router.push('/auth/signup'); 26 | } 27 | } 28 | } 29 | }; 30 | 31 | const socialSignup = async req => { 32 | try { 33 | await validIdAPI(req.id); 34 | await signupAPI(req); 35 | 36 | socialLogin(req, 'afterSignup'); 37 | } catch (error) { 38 | console.log(error); 39 | 40 | const confirmYn = confirm( 41 | '이미 가입된 소셜 회원입니다. \n로그인 화면으로 이동하시겠습니까?', 42 | ); 43 | if (confirmYn) { 44 | router.push('/auth/login'); 45 | } 46 | } 47 | }; 48 | 49 | export { socialLogin, socialSignup }; 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | # nodeJS 버전 4 | node_js: 5 | - 14.16.0 6 | 7 | # Git Commit 수신 허용 목록 master 브랜치만. 8 | branches: 9 | only: 10 | - master 11 | 12 | # Travis CI 에서 node_modules를 캐싱한다. 13 | # 빌드 프로세스 속도를 높여줌. 14 | cache: 15 | directories: 16 | - node_modules 17 | 18 | # Travis 서버에서 빌드 명령어 19 | script: npm run build 20 | 21 | # .env.production 파일 인코딩했던 것을 디코딩해서 만들어줌. 22 | before_install: 23 | - openssl aes-256-cbc -K $encrypted_538818e76846_key -iv $encrypted_538818e76846_iv 24 | -in .env.production.enc -out .env.production -d 25 | 26 | # AWS CloudFront 캐싱을 위해 설치. 27 | before_deploy: 28 | - npm install -g travis-ci-cloudfront-invalidation 29 | 30 | # build후 배포 대상 31 | deploy: 32 | provider: s3 33 | access_key_id: $AWS_ACCESS_KEY 34 | secret_access_key: $AWS_SECRET_KEY 35 | bucket: tripllo.tech 36 | skip_cleanup: true 37 | acl: public_read 38 | region: ap-northeast-2 39 | wait-until-deploy: true 40 | local_dir: dist 41 | 42 | # 배포 완료 후 AWS CloudFront의 캐시를 무효화. 43 | after_deploy: 44 | - aws configure set preview.cloudfront true 45 | - travis-ci-cloudfront-invalidation -a $AWS_ACCESS_KEY -s $AWS_SECRET_KEY -c $AWS_CLOUDFRONT_DIST_ID -i '/*' -b $TRAVIS_BRANCH -p $TRAVIS_PULL_REQUEST -o 'master' 46 | 47 | # 빌드 후 알림 48 | notifications: 49 | email: 50 | recipients: 51 | - pozafly@kakao.com 52 | -------------------------------------------------------------------------------- /src/utils/socket.js: -------------------------------------------------------------------------------- 1 | import SockJS from 'sockjs-client'; 2 | import store from '@/store'; 3 | import bus from '@/utils/bus'; 4 | 5 | let socketInstance = null; 6 | 7 | const socketConnect = () => { 8 | const baseURL = 9 | process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_URL : ''; 10 | 11 | try { 12 | if (socketInstance === null) { 13 | const serverURL = `${baseURL}/websocket?m_id=${store.state.user.id}`; 14 | console.log(`서버 연결 시도 --- ${serverURL}`); 15 | socketInstance = new SockJS(serverURL); 16 | 17 | socketInstance.onopen = () => { 18 | console.log('소켓 연결 완료'); 19 | socketReceive(); 20 | }; 21 | } 22 | } catch (error) { 23 | console.log(error, '소켓 연결 실패'); 24 | } 25 | }; 26 | 27 | const socketReceive = () => { 28 | socketInstance.onmessage = ({ data }) => { 29 | console.log('메세지 수신', data); 30 | bus.$emit('receive-message', data); 31 | 32 | try { 33 | store.dispatch('READ_PUSH_MESSAGE', store.state.user.id); 34 | } catch (error) { 35 | console.log(error); 36 | alert('푸시메세지를 읽어오지 못했습니다.'); 37 | } 38 | }; 39 | }; 40 | 41 | const socketSend = messageData => { 42 | try { 43 | socketInstance.send(messageData); 44 | } catch (e) { 45 | console.log(e); 46 | alert('초대 메세지가 전송되지 않았습니다.'); 47 | } 48 | }; 49 | 50 | export { socketConnect, socketSend }; 51 | -------------------------------------------------------------------------------- /src/api/checklistItem.js: -------------------------------------------------------------------------------- 1 | import { checklistItem } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} CreateChecklistItemInfo 5 | * @property {number} checklistId - ChecklistItem이 어느 Checklist에 속해있는지 구분 6 | * @property {string} item - ChecklistItem 하나의 item 명 7 | */ 8 | 9 | /** 10 | * @typedef {Object} UpdateChecklistItemInfo 11 | * @property {string} isChecked - 체크 여부 12 | * @property {string} item - ChecklistItem 하나의 item 명 13 | */ 14 | 15 | /** 16 | * 체크리스트 아이템 생성 17 | * @param {CreateChecklistItemInfo} createChecklistItemInfo 18 | * @returns {Promise} statusCode - 상태코드 (조회는 checklist에서 한번에 함) 19 | */ 20 | const createChecklistItemAPI = createChecklistItemInfo => 21 | checklistItem.post('/', createChecklistItemInfo); 22 | 23 | /** 24 | * 체크리스트 아이템 수정 25 | * @param {UpdateChecklistItemInfo} updateChecklistItemInfo 26 | * @returns {Promise} statusCode - 상태코드 27 | */ 28 | const updateChecklistItemAPI = (checklistItemId, updateChecklistItemInfo) => 29 | checklistItem.put(`/${checklistItemId}`, updateChecklistItemInfo); 30 | 31 | /** 32 | * 체크리스트 아이템 삭제 33 | * @param {number} checklistItemId - 체크리스트 아이템 ID 34 | * @returns {Promise} statusCode - 상태코드 35 | */ 36 | const deleteChecklistItemAPI = checklistItemId => 37 | checklistItem.delete(`/${checklistItemId}`); 38 | 39 | export { 40 | createChecklistItemAPI, 41 | updateChecklistItemAPI, 42 | deleteChecklistItemAPI, 43 | }; 44 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { setInterceptors } from '@/api/common/interceptors'; 3 | 4 | const environmentURL = 5 | process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_URL : ''; 6 | 7 | // header 붙이지 않음. login시 사용됨. 8 | const createInstance = () => { 9 | return axios.create({ 10 | baseURL: `${environmentURL}/api/`, 11 | }); 12 | }; 13 | 14 | // axios interceptor를 통해 header에 token 넣어주고 return 15 | const createInstanceWithAuth = url => { 16 | const instance = axios.create({ 17 | baseURL: `${environmentURL}/api/${url}`, 18 | }); 19 | return setInterceptors(instance); 20 | }; 21 | 22 | export const instance = createInstance(); 23 | export const board = createInstanceWithAuth('board'); 24 | export const list = createInstanceWithAuth('list'); 25 | export const card = createInstanceWithAuth('card'); 26 | export const checklist = createInstanceWithAuth('checklist'); 27 | export const checklistItem = createInstanceWithAuth('checklistItem'); 28 | export const comments = createInstanceWithAuth('comment'); 29 | export const pushMessage = createInstanceWithAuth('pushMessage'); 30 | export const upload = createInstanceWithAuth('upload'); 31 | export const email = createInstanceWithAuth('email'); 32 | export const boardHasLike = createInstanceWithAuth('boardHasLike'); 33 | export const hashtag = createInstanceWithAuth('hashtag'); 34 | export const user = createInstanceWithAuth('user'); 35 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/CardLabels.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | 41 | 64 | -------------------------------------------------------------------------------- /src/views/NotFoundPage.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 60 | -------------------------------------------------------------------------------- /src/api/pushMessage.js: -------------------------------------------------------------------------------- 1 | import { pushMessage } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} PushMessage 5 | * @property {number} id - Push Message ID 6 | * @property {number} boardId - 어느 Board에 초대했는지 구분 7 | * @property {string} content - 초대 내용 8 | * @property {string} isRead - 초대장을 읽었는지 여부 9 | * @property {string} senderId - 초대한 유저 ID 10 | * @property {string} targetId - 초대 받은 유저 ID(현재 로그인 한 유저) 11 | * @property {string} createdAt - 생성날짜 12 | * @property {string} createdBy - 생성자 13 | * @property {string} updatedAt - 수정날짜 14 | * @property {string} updatedBy - 수정자 15 | */ 16 | 17 | /** 18 | * @typedef {Object} UpdateMessageInfo 19 | * @property {string} id - 푸시 메세지 ID 20 | * @property {string} isRead - 초대장을 읽었는지 여부 21 | */ 22 | 23 | /** 24 | * 푸시 메세지 조회 25 | * @param {string} targetId - (로그인 한) 대상 유저 ID 26 | * @returns {Promise} 27 | */ 28 | const readPushMessageAPI = targetId => pushMessage.get(`/${targetId}`); 29 | 30 | /** 31 | * 푸시 메세지 수정 32 | * @param {UpdateMessageInfo} updateMessageInfo 33 | * @returns {Promise} statusCode - 상태코드 34 | */ 35 | const updatePushMessageAPI = updateMessageInfo => 36 | pushMessage.put('/', updateMessageInfo); 37 | 38 | /** 39 | * 푸시 메세지 삭제 40 | * @param {number} id - 푸시 메세지 ID 41 | * @returns {Promise} statusCode - 상태코드 42 | */ 43 | const deletePushMessageAPI = id => pushMessage.delete(`/${id}`); 44 | 45 | export { readPushMessageAPI, updatePushMessageAPI, deletePushMessageAPI }; 46 | -------------------------------------------------------------------------------- /src/features/filter.js: -------------------------------------------------------------------------------- 1 | // 시간 나열 filter 2 | const normalFormatDate = value => { 3 | const date = new Date(value); 4 | const year = date.getFullYear(); 5 | 6 | let month = date.getMonth() + 1; 7 | month = month > 9 ? month : `0${month}`; 8 | 9 | let day = date.getDate(); 10 | day = day > 9 ? day : `0${day}`; 11 | 12 | let hours = date.getHours(); 13 | 14 | let ampm = hours >= 12 ? 'pm' : 'am'; 15 | hours = hours % 12; 16 | hours = hours ? hours : 12; 17 | hours = hours > 9 ? hours : `0${hours}`; 18 | 19 | let minutes = date.getMinutes(); 20 | minutes = minutes > 9 ? minutes : `0${minutes}`; 21 | 22 | return `${year}-${month}-${day} ${hours}:${minutes} ${ampm}`; 23 | }; 24 | 25 | // 몇분전 filter 26 | const timeForToday = value => { 27 | const today = new Date(); 28 | const timeValue = new Date(value); 29 | 30 | const betweenTime = Math.floor( 31 | (today.getTime() - timeValue.getTime()) / 1000 / 60, 32 | ); 33 | if (betweenTime < 1) { 34 | return '방금전'; 35 | } 36 | if (betweenTime < 60) { 37 | return `${betweenTime}분전`; 38 | } 39 | 40 | const betweenTimeHour = Math.floor(betweenTime / 60); 41 | if (betweenTimeHour < 24) { 42 | return `${betweenTimeHour}시간전`; 43 | } 44 | 45 | const betweenTimeDay = Math.floor(betweenTime / 60 / 24); 46 | if (betweenTimeDay < 365) { 47 | return `${betweenTimeDay}일전`; 48 | } 49 | 50 | return `${Math.floor(betweenTimeDay / 365)}년전`; 51 | }; 52 | 53 | export const filters = Vue => { 54 | Vue.filter('normalFormatDate', normalFormatDate); 55 | Vue.filter('timeForToday', timeForToday); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/attachment/CardAttachment.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 60 | 61 | 69 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/checklists/CardChecklist.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 60 | 61 | 70 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/location/CardLocationMapPin.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 52 | 53 | 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tripllo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 13 | "@fortawesome/free-regular-svg-icons": "^5.15.1", 14 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 15 | "@fortawesome/vue-fontawesome": "^2.0.2", 16 | "@googlemaps/js-api-loader": "^1.11.1", 17 | "@googlemaps/markerclustererplus": "^1.0.3", 18 | "@sentry/tracing": "^6.2.5", 19 | "@sentry/vue": "^6.2.5", 20 | "axios": "^0.21.0", 21 | "core-js": "^3.6.5", 22 | "dragula": "^3.7.3", 23 | "k-progress": "^1.5.0", 24 | "lodash": "^4.17.20", 25 | "sockjs-client": "^1.5.0", 26 | "v-calendar": "^2.1.6", 27 | "v-click-outside": "^3.1.2", 28 | "vue": "^2.6.11", 29 | "vue-color": "^2.8.1", 30 | "vue-infinite-loading": "^2.4.5", 31 | "vue-meta": "^2.4.0", 32 | "vue-notification": "^1.3.20", 33 | "vue-plugin-load-script": "^1.3.2", 34 | "vue-router": "^3.4.9", 35 | "vuex": "^3.6.0" 36 | }, 37 | "devDependencies": { 38 | "@sentry/webpack-plugin": "^1.14.2", 39 | "@vue/cli-plugin-babel": "~4.5.0", 40 | "@vue/cli-plugin-eslint": "~4.5.0", 41 | "@vue/cli-service": "~4.5.0", 42 | "@vue/eslint-config-prettier": "^6.0.0", 43 | "babel-eslint": "^10.1.0", 44 | "eslint": "^6.7.2", 45 | "eslint-plugin-prettier": "^3.1.3", 46 | "eslint-plugin-vue": "^6.2.2", 47 | "node-sass": "^5.0.0", 48 | "prettier": "^1.19.1", 49 | "sass-loader": "^10.1.0", 50 | "vue-template-compiler": "^2.6.11" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/fontAwesomeIcon.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { library } from '@fortawesome/fontawesome-svg-core'; 4 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; 5 | 6 | import { 7 | faUser, 8 | faExchangeAlt, 9 | faTrashAlt, 10 | faAlignLeft, 11 | faCheckSquare, 12 | faMapMarkerAlt, 13 | faEdit, 14 | faHome, 15 | faSuitcase, 16 | faInfoCircle, 17 | faUserEdit, 18 | faRunning, 19 | faTag, 20 | faPaperclip, 21 | faClock, 22 | faSkull, 23 | faBell, 24 | faUserFriends, 25 | faClipboardList, 26 | faHashtag, 27 | faGlobeAmericas, 28 | faFillDrip, 29 | faPlusCircle, 30 | faLayerGroup, 31 | faHeart, 32 | faSearch, 33 | faCrown, 34 | faComment, 35 | } from '@fortawesome/free-solid-svg-icons'; 36 | library.add( 37 | faUser, 38 | faExchangeAlt, 39 | faTrashAlt, 40 | faAlignLeft, 41 | faCheckSquare, 42 | faMapMarkerAlt, 43 | faEdit, 44 | faHome, 45 | faSuitcase, 46 | faInfoCircle, 47 | faUserEdit, 48 | faRunning, 49 | faTag, 50 | faPaperclip, 51 | faClock, 52 | faSkull, 53 | faBell, 54 | faUserFriends, 55 | faClipboardList, 56 | faHashtag, 57 | faGlobeAmericas, 58 | faFillDrip, 59 | faPlusCircle, 60 | faLayerGroup, 61 | faHeart, 62 | faSearch, 63 | faCrown, 64 | faComment, 65 | ); 66 | 67 | import { 68 | faClock as farClock, 69 | faClipboard as farClipboard, 70 | faCheckSquare as farCheckSquare, 71 | faUser as farUser, 72 | faCheckCircle as farCheckCircle, 73 | faMeh as farMeh, 74 | faHeart as farHeart, 75 | } from '@fortawesome/free-regular-svg-icons'; 76 | library.add( 77 | farClock, 78 | farClipboard, 79 | farCheckSquare, 80 | farUser, 81 | farCheckCircle, 82 | farMeh, 83 | farHeart, 84 | ); 85 | 86 | Vue.component('awesome', FontAwesomeIcon); 87 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Tripllo - 체계적인 계획 공유 플랫폼 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/board/boardHeader/BoardHeaderDisclosureStatus.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 73 | -------------------------------------------------------------------------------- /src/utils/social/Kakao.js: -------------------------------------------------------------------------------- 1 | import { socialLogin, socialSignup } from '@/utils/social'; 2 | 3 | const Kakao = { 4 | init() { 5 | window.Kakao.init(process.env.VUE_APP_KAKAO_APP_KEY); 6 | return true; 7 | }, 8 | 9 | getInfo(authObj, division) { 10 | window.Kakao.API.request({ 11 | url: '/v2/user/me', 12 | success: async res => { 13 | const kakao_account = res.kakao_account; 14 | const req = { 15 | id: res.id, 16 | name: kakao_account.profile.nickname, 17 | email: kakao_account.email, 18 | picture: kakao_account.profile.profile_image_url, 19 | social: 'Kakao', 20 | }; 21 | if (division === 'login') { 22 | socialLogin(req); 23 | } else { 24 | socialSignup(req); 25 | } 26 | }, 27 | fail: error => { 28 | console.log(error); 29 | alert('카카오 로그인 사용자 정보를 가져오지 못했습니다.'); 30 | }, 31 | }); 32 | }, 33 | 34 | login() { 35 | window.Kakao.Auth.login({ 36 | scope: 'profile, account_email', 37 | success: authObj => { 38 | this.getInfo(authObj, 'login'); 39 | }, 40 | fail: error => { 41 | console.log(error); 42 | alert('카카오로 로그인 실패'); 43 | }, 44 | }); 45 | }, 46 | 47 | signup() { 48 | window.Kakao.Auth.login({ 49 | scope: 'profile, account_email', 50 | success: authObj => { 51 | this.getInfo(authObj, 'signup'); 52 | }, 53 | fail: error => { 54 | console.log(error); 55 | alert('카카오로 회원가입 실패'); 56 | }, 57 | }); 58 | }, 59 | 60 | logout() { 61 | window.Kakao.Auth.logout(res => { 62 | if (!res) { 63 | return console.log(error); 64 | } 65 | social_logout(); 66 | }); 67 | }, 68 | }; 69 | 70 | export default Kakao; 71 | -------------------------------------------------------------------------------- /src/css/common.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | font-family: -apple-system, BlinkMacSystemFont,'Segoe UI',Roboto,'Noto Sans','Ubuntu','Droid Sans','Helvetica Neue',sans-serif; 4 | font-weight: 450; 5 | } 6 | 7 | body { 8 | background: #f9fafc; 9 | margin: 0; 10 | padding: 0; 11 | height: 100%; 12 | overflow-y: scroll; 13 | } 14 | 15 | #app { 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | ul, 21 | ol, 22 | li { 23 | margin: 0; 24 | padding: 0; 25 | list-style: none; 26 | } 27 | 28 | /*--- LINK ---*/ 29 | h1 { 30 | text-align: center; 31 | font-weight: 100; 32 | } 33 | 34 | h2, 35 | h3, 36 | h4, 37 | h5, 38 | h6 { 39 | margin: 0; 40 | font-size: inherit; 41 | font-weight: inherit; 42 | } 43 | 44 | a { 45 | color: #0282ce; 46 | text-decoration: none; 47 | &:hover { 48 | color: #3fc1c9; 49 | } 50 | } 51 | button { 52 | cursor: pointer; 53 | border: 0; 54 | border-radius: 3px; 55 | box-shadow: rgba(0, 0, 0, 0.2) 1px 1px 5px 0; 56 | background: #5aac44; 57 | color: white; 58 | &:hover { 59 | background: #60bd4e; 60 | } 61 | &:disabled { 62 | background: #ccc; 63 | cursor: default; 64 | } 65 | } 66 | input { 67 | font-size: 14px; 68 | background-color: #FAFBFC; 69 | border: 2px solid #DFE1E6; 70 | box-sizing: border-box; 71 | border-radius: 3px; 72 | padding:0px 0px 0px 15px; 73 | } 74 | .form-control { 75 | width: 100%; 76 | box-sizing: border-box; 77 | background-color: #e2e4e6; 78 | border: 1px solid #cdd2d4; 79 | border-radius: 3px; 80 | display: block; 81 | margin-bottom: 12px; 82 | padding: 6px 8px; 83 | transition: background-color 0.3s; 84 | } 85 | input[type='text'].form-control, 86 | input[type='password'].form-control, 87 | textarea.form-control { 88 | font-size: 14px; 89 | } 90 | .form-control:focus { 91 | background-color: #fff; 92 | } -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import { 2 | saveUserToLocalStorage, 3 | saveSessionStorage, 4 | saveTokenToLocalStorage, 5 | } from '@/utils/webStorage'; 6 | 7 | const mutations = { 8 | // user 9 | setUser(state, user) { 10 | saveUserToLocalStorage(user); 11 | state.user = user; 12 | }, 13 | setUserToken(state, token) { 14 | saveSessionStorage('mainTabId', 0); 15 | saveTokenToLocalStorage(token); 16 | state.token = token; 17 | }, 18 | logout(state) { 19 | state.user = {}; 20 | localStorage.clear(); 21 | sessionStorage.clear(); 22 | }, 23 | 24 | // board 25 | pushPersonalBoard(state, data) { 26 | state.personalBoard = state.personalBoard.concat(data); 27 | }, 28 | resetPersonalBoard(state) { 29 | state.personalBoard = []; 30 | }, 31 | readPersonalBoardLimitCount(state, data) { 32 | state.personalBoard = data; 33 | }, 34 | setRecentBoard(state, recentBoard) { 35 | saveSessionStorage('recentBoard', recentBoard); 36 | state.recentBoard = recentBoard; 37 | }, 38 | setBoardDetail(state, board) { 39 | saveSessionStorage('board', board); 40 | state.board = board; 41 | }, 42 | setTheme(state, bgColor) { 43 | saveSessionStorage('bgColor', bgColor); 44 | state.bgColor = bgColor ? bgColor : 'rgb(0, 121, 191);'; 45 | }, 46 | 47 | // card 48 | setCard(state, card) { 49 | saveSessionStorage('card', card); 50 | state.card = card; 51 | }, 52 | 53 | // pushMessage 54 | setPushMessage(state, pushMessage) { 55 | saveSessionStorage('pushMessage', pushMessage); 56 | state.pushMessage = pushMessage; 57 | }, 58 | deletePushMessage(state) { 59 | state.pushMessage = ''; 60 | }, 61 | 62 | // file upload 63 | setFile(state, file) { 64 | state.file = file; 65 | }, 66 | deleteFile(state) { 67 | state.file = []; 68 | }, 69 | }; 70 | 71 | export default mutations; 72 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalChecklist.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 62 | 63 | 76 | -------------------------------------------------------------------------------- /src/utils/social/Google.js: -------------------------------------------------------------------------------- 1 | import { socialLogin, socialSignup } from '@/utils/social'; 2 | import router from '@/routes'; 3 | import { getUserFromLocalStorage } from '@/utils/webStorage'; 4 | 5 | const Google = { 6 | init() { 7 | window.gapi.load('auth2', () => { 8 | const auth2 = window.gapi.auth2.init({ 9 | client_id: process.env.VUE_APP_GOOGLE_CLIENT_ID, 10 | cookiepolicy: 'single_host_origin', 11 | }); 12 | this.attachSignin(document.getElementById('google-login-btn'), auth2); 13 | }); 14 | }, 15 | 16 | attachSignin(element, auth2) { 17 | auth2.attachClickHandler( 18 | element, 19 | {}, 20 | googleUser => { 21 | const profile = googleUser.getBasicProfile(); 22 | 23 | if (getUserFromLocalStorage()) { 24 | alert('이미 로그인 되어 있습니다.'); 25 | router.push('/main'); 26 | return; 27 | } 28 | const path = router.history.current.path; 29 | if (path.includes('login')) { 30 | this.login(profile); 31 | } else { 32 | this.signup(profile); 33 | } 34 | }, 35 | error => { 36 | // alert(JSON.stringify(error, undefined, 2)); 37 | console.log(JSON.stringify(error, undefined, 2)); 38 | }, 39 | ); 40 | }, 41 | 42 | login(profile) { 43 | this.makeReq(profile).then(req => { 44 | socialLogin(req); 45 | }); 46 | }, 47 | 48 | signup(profile) { 49 | this.makeReq(profile).then(req => { 50 | socialSignup(req); 51 | }); 52 | }, 53 | 54 | makeReq(profile) { 55 | return new Promise((resolve, reject) => { 56 | const req = { 57 | name: profile.getName(), 58 | id: profile.getEmail(), 59 | email: profile.getEmail(), 60 | picture: profile.getImageUrl(), 61 | social: 'Google', 62 | }; 63 | resolve(req); 64 | }); 65 | }, 66 | }; 67 | 68 | export default Google; 69 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true 6 | }, 7 | extends: ["plugin:vue/recommended", "@vue/prettier"], 8 | rules: { 9 | "no-console": "off", 10 | // "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 11 | // "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 12 | "prettier/prettier": ['error', { 13 | singleQuote: true, 14 | semi: true, 15 | useTabs: false, 16 | tabWidth: 2, 17 | trailingComma: 'all', 18 | printWidth: 80, 19 | bracketSpacing: true, 20 | arrowParens: 'avoid', 21 | }], 22 | 'vue/no-timplate-key': 'off', 23 | 'vue/order-in-components': ['error', { 24 | "order": [ 25 | "el", 26 | "name", 27 | "key", 28 | "parent", 29 | "functional", 30 | ["delimiters", "comments"], 31 | ["components", "directives", "filters"], 32 | "extends", 33 | "mixins", 34 | ["provide", "inject"], 35 | "ROUTER_GUARDS", 36 | "layout", 37 | "middleware", 38 | "validate", 39 | "scrollToTop", 40 | "transition", 41 | "loading", 42 | "inheritAttrs", 43 | "model", 44 | ["props", "propsData"], 45 | "emits", 46 | "setup", 47 | "asyncData", 48 | "data", 49 | "fetch", 50 | "head", 51 | "computed", 52 | "watch", 53 | "watchQuery", 54 | "LIFECYCLE_HOOKS", 55 | "methods", 56 | ["template", "render"], 57 | "renderError" 58 | ], 59 | }], 60 | }, 61 | parserOptions: { 62 | parser: "babel-eslint" 63 | }, 64 | overrides: [ 65 | { 66 | files: [ 67 | "**/__tests__/*.{j,t}s?(x)", 68 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 69 | ], 70 | env: { 71 | jest: true 72 | } 73 | } 74 | ] 75 | }; -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/location/CardLocationMapBase.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 85 | -------------------------------------------------------------------------------- /src/api/comment.js: -------------------------------------------------------------------------------- 1 | import { comments } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} Comment 5 | * @property {number} id - Comment ID 6 | * @property {number} cardId - Comment가 어느 Card에 속해있는지 구분 7 | * @property {string} comment - Comment(댓글) 8 | * @property {number} dept - 대댓글 여부(깊이) : 0|1 9 | * @property {number} groupNum - 댓글, 대댓글 그룹 넘버 10 | * @property {string} deleteYn - 삭제 여부. 만약 대댓글이 존재할 경우 실제 삭제가 아닌 삭제 표시만 11 | * @property {string} userId - Comment 유저 ID 12 | * @property {string} picture - 사용자 프로필 이미지 경로 13 | * @property {string} createdAt - 생성날짜 14 | * @property {string} createdBy - 생성자 15 | * @property {string} updatedAt - 수정날짜 16 | * @property {string} updatedBy - 수정자 17 | */ 18 | 19 | /** 20 | * @typedef {Object} CreateCommentInfo 21 | * @property {number} cardId - Comment가 어느 Card에 속해있는지 구분 22 | * @property {string} comment - Comment(댓글) 23 | * @property {number} dept - 대댓글 여부(깊이) : 0|1 24 | * @property {number} groupNum - 댓글, 대댓글 그룹 넘버 25 | */ 26 | 27 | /** 28 | * @typedef {Object} UpdateCommentInfo 29 | * @property {number} id - Comment ID 30 | * @property {string} comment - 수정될 코멘트 31 | */ 32 | 33 | /** 34 | * 코멘트 생성 35 | * @param {CreateCommentInfo} createCommentInfo 36 | * @returns {Promise} statusCode - 상태코드 37 | */ 38 | const createCommentAPI = createCommentInfo => 39 | comments.post(`/`, createCommentInfo); 40 | 41 | /** 42 | * 코멘트 조회 43 | * @param {number} cardId - Card ID 44 | * @returns {Promise} 45 | */ 46 | const readCommentAPI = cardId => comments.get(`/${cardId}`); 47 | 48 | /** 49 | * 코멘트 수정 50 | * @param {UpdateCommentInfo} updateCommentInfo 51 | * @returns {Promise} statusCode - 상태코드 52 | */ 53 | const updateCommentAPI = updateCommentInfo => 54 | comments.put(`/`, updateCommentInfo); 55 | 56 | /** 57 | * 코멘트 삭제 58 | * @param {number} id - Comment ID 59 | * @returns {Promise} statusCode - 상태코드 60 | */ 61 | const deleteCommentAPI = id => comments.delete(`/${id}`); 62 | 63 | export { createCommentAPI, readCommentAPI, updateCommentAPI, deleteCommentAPI }; 64 | -------------------------------------------------------------------------------- /src/api/auth.js: -------------------------------------------------------------------------------- 1 | // 로그인 API 2 | import { instance } from '@/api'; 3 | 4 | /** 5 | * @typedef {Object} LoginData 6 | * @property {string} id - 로그인 아이디 7 | * @property {string} password - 로그인 비밀번호 8 | */ 9 | 10 | /** 11 | * @typedef {Object} User 12 | * @property {string} bio - 소개 13 | * @property {string} email - 이메일 14 | * @property {string} id - 아이디 15 | * @property {number[]} invitedBoard - 초대된 Board 16 | * @property {string} name - 이름 17 | * @property {string} picture - 프로필 이미지 경로 18 | * @property {number[]} recentBoard - 최근 본 Board 19 | * @property {string} social - 소셜 계정 여부. 소셜 가입이 아닐 때는 null 20 | * @property {string} token - 로그인 token 21 | * @property {string} createdAt - 생성날짜 22 | * @property {string} createdBy - 생성자 23 | * @property {string} updatedAt - 수정날짜 24 | * @property {string} updatedBy - 수정자 25 | */ 26 | 27 | /** 28 | * @typedef {Object} SignupData 29 | * @property {string} id - 회원가입 아이디 30 | * @property {string} password - 회원가입 비밀번호 31 | * @property {string} email - 회원가입 이메일 32 | * @property {string} name - 회원가입 이름 33 | */ 34 | 35 | /** 36 | * id, pw를 이용해 로그인 37 | * @param {LoginData} loginData 38 | * @returns {Promise} userData 39 | */ 40 | const loginUserAPI = loginData => instance.post('login', loginData); 41 | 42 | /** 43 | * 소셜 로그인을 이용해 로그인 44 | * @param {string} userId 45 | * @returns {Promise} userData 46 | */ 47 | const socialLoginAPI = userId => instance.get(`login/social/${userId}`); 48 | 49 | /** 50 | * 로그아웃 51 | * @returns {Promise} statusCode - 상태코드 52 | */ 53 | const logoutUserAPI = () => instance.get('logout'); 54 | 55 | /** 56 | * 회원가입 57 | * @param {SignupData} signupData - 회원가입 Data 58 | * @returns {Promise} isSignup 59 | */ 60 | const signupAPI = signupData => instance.post('user', signupData); 61 | 62 | /** 63 | * 회원가입 화면에서 회원ID가 사용되고 있는지 실시간 판별 64 | * @param {string} userId - 회원ID 65 | * @returns {Promise} statusCode - 상태코드 66 | */ 67 | const validIdAPI = userId => instance.get(`user/valid/${userId}`); 68 | 69 | export { loginUserAPI, socialLoginAPI, logoutUserAPI, signupAPI, validIdAPI }; 70 | -------------------------------------------------------------------------------- /src/components/common/MiniModal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 81 | -------------------------------------------------------------------------------- /src/api/card.js: -------------------------------------------------------------------------------- 1 | import { card } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} CardDetail 5 | * @property {string} description - 메모(설명) 6 | * @property {string} dueDate - 날짜 체크 값 7 | * @property {number} id - card의 ID 8 | * @property {string[]} labelColor - 레이블 색상 9 | * @property {number} title - Card 제목 10 | * @property {string} listId - card가 속해있는 list의 ID 11 | * @property {string} location - 장소 위치 값 12 | * @property {number} pos - Dragula 포지션 값 13 | * @property {string} createdAt - 생성날짜 14 | * @property {string} createdBy - 생성자 15 | * @property {string} updatedAt - 수정날짜 16 | * @property {string} updatedBy - 수정자 17 | */ 18 | 19 | /** 20 | * @typedef {Object} CreateCardInfo 21 | * @property {number} title - Card 제목 22 | * @property {number} listId - Card가 어느 list에 속해있는지 구분 23 | * @property {number} pos - Dragula 포지션 값 24 | */ 25 | 26 | /** 27 | * @typedef {Object} UpdateCardInfo 28 | * @property {number} id - Card ID 29 | * @property {number} title - Card 제목 30 | * @property {number} pos - Dragula 포지션 값 31 | * @property {string} description - 메모(설명) 32 | * @property {string[]} labelColor - 레이블 색상 33 | * @property {string} location - 장소 위치 값 34 | * @property {string} dueDate - 날짜 체크 값 35 | * @property {string} listId - card가 속해있는 list의 ID 36 | */ 37 | 38 | /** 39 | * 카드 생성 40 | * @param {CreateCardInfo} createCardInfo 41 | * @returns {Promise} statusCode - 상태코드 42 | */ 43 | const createCardAPI = createCardInfo => card.post('/', createCardInfo); 44 | 45 | /** 46 | * 카드 상세 조회 47 | * @param {number} id - 카드 ID 48 | * @returns {Promise} 49 | */ 50 | const readCardAPI = id => card.get(`/${id}`); 51 | 52 | /** 53 | * 카드 수정 54 | * @param {number} id - 카드 ID 55 | * @param {UpdateCardInfo} updateCardInfo 56 | * @returns {Promise} statusCode - 상태코드 57 | */ 58 | const updateCardAPI = (id, updateCardInfo) => 59 | card.put(`/${id}`, updateCardInfo); 60 | 61 | /** 62 | * 카드 삭제 63 | * @param {number} id - 카드 ID 64 | * @returns {Promise} statusCode - 상태코드 65 | */ 66 | const deleteCardAPI = id => card.delete(`/${id}`); 67 | 68 | export { createCardAPI, readCardAPI, updateCardAPI, deleteCardAPI }; 69 | -------------------------------------------------------------------------------- /src/views/MainPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 65 | 66 | 85 | -------------------------------------------------------------------------------- /src/components/board/boardHeader/boardMenu/BoardMenuColorPicker.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | 65 | 91 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import { firstAccess, requireAuth, intoBoard } from '@/routes/navigationGuard'; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const router = new VueRouter({ 8 | mode: 'history', 9 | routes: [ 10 | { path: '/', beforeEnter: firstAccess }, 11 | { 12 | path: '/intro', 13 | component: () => import('@/views/IntroPage.vue'), 14 | }, 15 | { 16 | path: '/privacy', 17 | component: () => import('@/views/PrivacyPage.vue'), 18 | }, 19 | { 20 | path: '/manual', 21 | component: () => import('@/views/ManualPage.vue'), 22 | }, 23 | { 24 | path: '/main', 25 | component: () => import('@/views/MainPage.vue'), 26 | beforeEnter: requireAuth, 27 | }, 28 | { 29 | // 중첩된 라우트 : 한 페이지에 url에 따라서 다른 컴포넌트를 보여야 할 때 사용. 30 | path: '/auth', 31 | redirect: '/auth/login', 32 | component: () => import('@/views/AuthPage.vue'), 33 | children: [ 34 | { 35 | path: 'login', 36 | name: 'login', 37 | component: () => import('@/components/auth/LoginForm.vue'), 38 | }, 39 | { 40 | path: 'signup', 41 | component: () => import('@/components/auth/SignupForm.vue'), 42 | }, 43 | { 44 | path: 'findPassword', 45 | component: () => import('@/components/auth/FindPassword.vue'), 46 | }, 47 | ], 48 | }, 49 | { 50 | path: '/board/:boardId', 51 | component: () => import('@/views/BoardPage.vue'), 52 | beforeEnter: intoBoard, 53 | children: [ 54 | { 55 | path: 'card/:cardId', 56 | component: () => import('@/components/card/cardModal/CardModal.vue'), 57 | beforeEnter: requireAuth, 58 | }, 59 | ], 60 | }, 61 | { 62 | path: '/profile', 63 | component: () => import('@/views/ProfilePage.vue'), 64 | beforeEnter: requireAuth, 65 | }, 66 | { 67 | path: '/user/:userId', 68 | component: () => import('@/views/UserBoardPage.vue'), 69 | beforeEnter: requireAuth, 70 | }, 71 | { path: '*', component: () => import('@/views/NotFoundPage.vue') }, 72 | ], 73 | }); 74 | 75 | export default router; 76 | -------------------------------------------------------------------------------- /src/utils/social/Facebook.js: -------------------------------------------------------------------------------- 1 | import { socialLogin, socialSignup } from '@/utils/social'; 2 | 3 | const Facebook = { 4 | init() { 5 | window.fbAsyncInit = function() { 6 | window.FB.init({ 7 | appId: process.env.VUE_APP_FACEBOOK_APP_KEY, 8 | cookie: true, 9 | xfbml: true, 10 | version: 'v9.0', 11 | }); 12 | window.FB.AppEvents.logPageView(); 13 | }; 14 | (function(d, s, id) { 15 | var js, 16 | fjs = d.getElementsByTagName(s)[0]; 17 | if (d.getElementById(id)) { 18 | return; 19 | } 20 | js = d.createElement(s); 21 | js.id = id; 22 | js.src = 'https://connect.facebook.net/en_US/sdk.js'; 23 | fjs.parentNode.insertBefore(js, fjs); 24 | })(document, 'script', 'facebook-jssdk'); 25 | }, 26 | 27 | async login() { 28 | const req = await this.getInfo(); 29 | await socialLogin(req); 30 | }, 31 | 32 | async signup() { 33 | const req = await this.getInfo(); 34 | await socialSignup(req); 35 | }, 36 | 37 | getInfo() { 38 | return new Promise((resolve, reject) => { 39 | window.FB.getLoginStatus(() => { 40 | // 첫 시도 41 | window.FB.login( 42 | response => { 43 | if (response.status === 'connected') { 44 | // const accessToken = response.authResponse.accessToken; 45 | window.FB.api( 46 | '/me', 47 | { fields: 'id, name, email, picture' }, 48 | res => { 49 | if (!res) { 50 | this.LoginFailure(); 51 | } 52 | const req_body = { 53 | id: res.id, 54 | name: res.name, 55 | email: res.email, 56 | picture: res.picture.data.url, 57 | social: 'Facebook', 58 | }; 59 | resolve(req_body); 60 | }, 61 | ); 62 | } else { 63 | this.LoginFailure(); 64 | } 65 | }, 66 | { scope: 'public_profile, email' }, 67 | ); 68 | }); 69 | }); 70 | }, 71 | LoginFailure() { 72 | alert('페이스북 로그인 실패'); 73 | }, 74 | }; 75 | 76 | export default Facebook; 77 | -------------------------------------------------------------------------------- /src/components/list/AddList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 75 | 76 | 93 | -------------------------------------------------------------------------------- /src/api/checklist.js: -------------------------------------------------------------------------------- 1 | import { checklist } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} Checklist 5 | * @property {number} cardId - Checklist가 어느 Card에 속해있는지 구분 6 | * @property {number} id - Checklist ID 7 | * @property {ChecklistItem[]} item - Checklist 안에 있는 item 목록 8 | * @property {number} title - Checklist 제목 9 | * @property {string} createdAt - 생성날짜 10 | * @property {string} createdBy - 생성자 11 | * @property {string} updatedAt - 수정날짜 12 | * @property {string} updatedBy - 수정자 13 | */ 14 | 15 | /** 16 | * @typedef {Object} ChecklistItem 17 | * @property {number} checklistId - ChecklistItem이 어느 Checklist에 속해있는지 구분 18 | * @property {number} id - ChecklistItem ID 19 | * @property {string} isChecked - 완료 여부 20 | * @property {string} item - ChecklistItem 하나의 item 명 21 | * @property {string} createdAt - 생성날짜 22 | * @property {string} createdBy - 생성자 23 | * @property {string} updatedAt - 수정날짜 24 | * @property {string} updatedBy - 수정자 25 | */ 26 | 27 | /** 28 | * @typedef {Object} CreateChecklistInfo 29 | * @property {string} title - 제목 30 | * @property {number} cardId - Checklist가 어느 Card에 속해있는지 구분 31 | */ 32 | 33 | /** 34 | * 체크리스트 생성 35 | * @param {CreateChecklistInfo} createChecklistInfo 36 | * @returns {Promise} statusCode - 상태코드 37 | */ 38 | const createChecklistAPI = createChecklistInfo => 39 | checklist.post('/', createChecklistInfo); 40 | 41 | /** 42 | * 체크리스트 조회 43 | * @param {number} id - 체크리스트 ID 44 | * @returns {Promise} 45 | */ 46 | const readChecklistAPI = id => checklist.get(`/${id}`); 47 | 48 | /** 49 | * 체크리스트 수정 50 | * @param {number} id - 체크리스트 ID 51 | * @param {string} title - 체크리스트 제목 52 | * @returns {Promise} statusCode - 상태코드 53 | */ 54 | const updateChecklistAPI = (id, title) => checklist.put(`/${id}`, title); 55 | 56 | /** 57 | * 체크리스트 수정 58 | * @param {number} checklistId - 체크리스트 ID 59 | * @param {number} cardId - Checklist가 어느 Card에 속해있는지 구분 60 | * @returns {Promise} statusCode - 상태코드 61 | */ 62 | const deleteChecklistAPI = ({ checklistId, cardId }) => 63 | // SpringBoot의 DeleteMapping에서 @PathVariable 때문에 payload(객체 전달) 불가 64 | checklist.delete(`/${checklistId}/${cardId}`); 65 | 66 | export { 67 | createChecklistAPI, 68 | readChecklistAPI, 69 | updateChecklistAPI, 70 | deleteChecklistAPI, 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/board/addBoard/AddBoardModalBase.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 87 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { loginUserAPI, logoutUserAPI } from '@/api/auth'; 2 | import { 3 | readPersonalBoardLimitCountAPI, 4 | readRecentBoardAPI, 5 | readBoardDetailAPI, 6 | } from '@/api/board'; 7 | import { readCardAPI } from '@/api/card'; 8 | import { readPushMessageAPI } from '@/api/pushMessage'; 9 | import { readFileAPI } from '@/api/upload'; 10 | import { signoutAPI, readUserAPI } from '@/api/user'; 11 | 12 | const actions = { 13 | // auth 14 | async LOGIN({ commit }, loginData) { 15 | const { data } = await loginUserAPI(loginData); 16 | commit('setUserToken', data.data.token); 17 | commit('setUser', data.data); 18 | }, 19 | LOGOUT({ commit }) { 20 | logoutUserAPI(); 21 | commit('logout'); 22 | }, 23 | 24 | // user 25 | SIGNOUT({ commit }, password) { 26 | signoutAPI(password); 27 | commit('logout'); 28 | }, 29 | async READ_USER({ commit }, userId) { 30 | const { data } = await readUserAPI(userId); 31 | commit('setUser', data.data); 32 | }, 33 | 34 | // board 35 | /* personal 탭에서 Recently Viewed와 My Boards의 36 | 좋아요 표시 연동 때문에 READ_PERSONAL_BOARD_LIMIT_COUNT 액션이 필요함. */ 37 | async READ_PERSONAL_BOARD_LIMIT_COUNT({ commit }, count) { 38 | const { data } = await readPersonalBoardLimitCountAPI(count); 39 | commit('readPersonalBoardLimitCount', data.data); 40 | }, 41 | async READ_RECENT_BOARD({ commit }, recentLists) { 42 | const { data } = await readRecentBoardAPI(recentLists); 43 | commit('setRecentBoard', data.data); 44 | }, 45 | async READ_BOARD_DETAIL({ commit }, boardId) { 46 | const { data } = await readBoardDetailAPI(boardId); 47 | commit('setBoardDetail', data.data); 48 | }, 49 | 50 | // card 51 | async READ_CARD({ commit }, id) { 52 | const { data } = await readCardAPI(id); 53 | await commit('setCard', data.data); 54 | return data.data; 55 | }, 56 | 57 | // pushMessage 58 | async READ_PUSH_MESSAGE({ commit }, targetId) { 59 | const { data } = await readPushMessageAPI(targetId); 60 | commit('setPushMessage', data.data); 61 | }, 62 | 63 | // upload 64 | async READ_FILE({ commit }, cardId) { 65 | const { data } = await readFileAPI(cardId); 66 | commit('setFile', data.data); 67 | }, 68 | DELETE_FILE_FROM_STATE({ commit }) { 69 | commit('deleteFile'); 70 | }, 71 | }; 72 | 73 | export default actions; 74 | -------------------------------------------------------------------------------- /src/components/mainPage/MainTab.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 54 | 55 | 96 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalLocation.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import { user } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} User 5 | * @property {string} bio - 소개 6 | * @property {string} email - 이메일 7 | * @property {string} id - 아이디 8 | * @property {number[]} invitedBoard - 초대된 Board 9 | * @property {string} name - 이름 10 | * @property {string} picture - 프로필 이미지 경로 11 | * @property {number[]} recentBoard - 최근 본 Board 12 | * @property {string} social - 소셜 계정 여부. 소셜 가입이 아닐 때는 null 13 | * @property {string} token - 로그인 token 14 | * @property {string} createdAt - 생성날짜 15 | * @property {string} createdBy - 생성자 16 | * @property {string} updatedAt - 수정날짜 17 | * @property {string} updatedBy - 수정자 18 | */ 19 | 20 | /** 21 | * @typedef {Object} UpdateUserInfo 22 | * @property {string} id - 유저 ID 23 | * @property {string} email - 이메일 24 | * @property {string} name - 이름 25 | * @property {string} password - 비밀번호 26 | * @property {string} bio - 소개 27 | * @property {string} picture - 프로필 이미지 경로 28 | * @property {number[]} recentBoard - 최근 본 Board 29 | * @property {number[]} invitedBoard - 초대된 Board 30 | */ 31 | 32 | /** 33 | * @typedef {Object} ChangePasswordInfo 34 | * @property {string} currentPw - 현재 비밀번호 35 | * @property {string} newPw - 변경 될 비밀번호 36 | */ 37 | 38 | /** 39 | * Board 페이지의 Invite Modal에서, 초대할 사람 목록 조회 40 | * @param {string} userId - 초대할 유저 ID 41 | * @returns {Promise} 42 | */ 43 | const readIsInviteUserForModalAPI = userId => user.get(`isInvite/${userId}`); 44 | 45 | /** 46 | * Board 페이지에서, 초대된 사람 47 | * @param {string[]} userList - 초대된 유저 ID 리스트 48 | * @returns {Promise} 49 | */ 50 | const readInvitedUserForBoardPageAPI = userList => 51 | user.get(`invited/${userList}`); 52 | 53 | /** 54 | * 회원 탈퇴 55 | * @param {string} password - 비밀번호 56 | * @returns {Promise} statusCode - 상태코드 57 | */ 58 | const signoutAPI = password => user.delete(`${password}`); 59 | 60 | /** 61 | * 회원 정보 수정시, 비밀번호 변경 시, 프로필 사진 변경 시 User 정보 재조회 62 | * @param {string} userId - 로그인 한 유저 ID 63 | * @returns {Promise} 64 | */ 65 | const readUserAPI = userId => user.get(`${userId}`); 66 | 67 | /** 68 | * 유저 수정 69 | * @param {UpdateUserInfo} updateUserInfo 70 | * @returns {Promise} statusCode - 상태코드 71 | */ 72 | const updateUserAPI = updateUserInfo => user.put('', updateUserInfo); 73 | 74 | /** 75 | * 비밀번호 변경 76 | * @param {ChangePasswordInfo} changePasswordInfo 77 | * @returns {Promise} statusCode - 상태코드 78 | */ 79 | const changePasswordAPI = changePasswordInfo => 80 | user.post('changePw', changePasswordInfo); 81 | 82 | export { 83 | readIsInviteUserForModalAPI, 84 | readInvitedUserForBoardPageAPI, 85 | signoutAPI, 86 | readUserAPI, 87 | updateUserAPI, 88 | changePasswordAPI, 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/mainPage/section/InvitedSection.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 64 | 65 | 108 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalAttachment.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 83 | 84 | 100 | -------------------------------------------------------------------------------- /src/utils/webStorage.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from '@/utils/libs'; 2 | 3 | const USER_INFO = 'TRIPLLO-V1-U'; 4 | const TOKEN = 'TRIPLLO-V1-T'; 5 | 6 | // NOTE: 인코딩, 디코딩 함수 7 | const makeEncode = (key, value) => { 8 | if (isEmpty(value)) { 9 | return; 10 | } 11 | const data = JSON.stringify(value); 12 | const encode = encodeURIComponent(data); 13 | localStorage.setItem(key, btoa(encode)); 14 | }; 15 | 16 | const returnDecode = value => { 17 | if (isEmpty(value)) { 18 | return; 19 | } 20 | const decode = atob(value); 21 | const data = JSON.parse(decodeURIComponent(decode)); 22 | return data; 23 | }; 24 | 25 | // 로컬스토리지 관련 함수(user) 26 | /** 27 | * @typedef {object} User 28 | * @property {string} id - 유저 ID 29 | * @property {string} email - 유저 이메일 30 | * @property {string} name - 유저 이름 31 | * @property {string} social - 소셜 가입 구분코드 32 | * @property {string} bio - 유저 소개 33 | * @property {string} picture - 유저 프로필 이미지 경로 34 | * @property {string} recentBoard - 최근에 본 Board 리스트(string[] 아님) 35 | * @property {string} invitedBoard - 초대된 Board 리스트(string[] 아님) 36 | */ 37 | 38 | /** 39 | * 유저 정보를 인코딩하여 localStorage에 올려줌 40 | * @param {User} user 41 | */ 42 | const saveUserToLocalStorage = user => { 43 | if (isEmpty(user)) { 44 | return; 45 | } 46 | return makeEncode(USER_INFO, user); 47 | }; 48 | 49 | /** 50 | * 로컬 스토리지에 올라가 있는 유저 정보를 디코딩하여 return 51 | * @returns {User} userInfo 52 | */ 53 | const getUserFromLocalStorage = () => { 54 | const userInfo = localStorage.getItem(USER_INFO); 55 | if (isEmpty(userInfo)) { 56 | return; 57 | } 58 | return returnDecode(userInfo); 59 | }; 60 | 61 | /** 62 | * 로그인 토큰을 인코딩하여 localStorage에 올려줌 63 | * @param {string} token 64 | */ 65 | const saveTokenToLocalStorage = token => { 66 | if (isEmpty(token)) { 67 | return; 68 | } 69 | makeEncode(TOKEN, token); 70 | }; 71 | 72 | /** 73 | * 로컬 스토리지에 올라가 있는 토큰 정보를 디코딩하여 return 74 | * @returns {string} tokenInfo 75 | */ 76 | const getTokenFromLocalStorage = () => { 77 | const tokenInfo = localStorage.getItem(TOKEN); 78 | if (isEmpty(tokenInfo)) { 79 | return; 80 | } 81 | return returnDecode(tokenInfo); 82 | }; 83 | 84 | const saveSessionStorage = (key, value) => { 85 | if (isEmpty(value)) { 86 | return; 87 | } 88 | sessionStorage.setItem(key, JSON.stringify(value)); 89 | }; 90 | 91 | const getSessionStorage = key => { 92 | if (isEmpty(key)) { 93 | return; 94 | } 95 | return JSON.parse(sessionStorage.getItem(key)); 96 | }; 97 | 98 | const deleteSessionStorage = key => { 99 | if (isEmpty(key)) { 100 | return; 101 | } 102 | sessionStorage.removeItem(key); 103 | }; 104 | 105 | export { 106 | saveUserToLocalStorage, 107 | getUserFromLocalStorage, 108 | saveTokenToLocalStorage, 109 | getTokenFromLocalStorage, 110 | saveSessionStorage, 111 | getSessionStorage, 112 | deleteSessionStorage, 113 | }; 114 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/CardDueDate.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 62 | 63 | 115 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalDueDate.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 95 | 96 | 116 | -------------------------------------------------------------------------------- /src/components/board/boardHeader/boardInvite/InviteModalList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 87 | 88 | 120 | -------------------------------------------------------------------------------- /src/components/common/AlertNotification.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 48 | 49 | 127 | -------------------------------------------------------------------------------- /src/components/card/cardModal/CardModalBase.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 60 | 61 | 140 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModalLabels.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 97 | 98 | 123 | -------------------------------------------------------------------------------- /src/components/board/boardHeader/boardInvite/InviteModal.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 105 | 106 | 122 | -------------------------------------------------------------------------------- /src/views/AuthPage.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 79 | 80 | 146 | -------------------------------------------------------------------------------- /src/components/card/AddCard.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 101 | 102 | 146 | -------------------------------------------------------------------------------- /src/utils/dragger/index.js: -------------------------------------------------------------------------------- 1 | import dragula from 'dragula'; 2 | import { updateListAPI } from '@/api/list'; 3 | import { updateCardAPI } from '@/api/card'; 4 | import './dragula.css'; 5 | import store from '@/store'; 6 | 7 | let cDragger = ''; 8 | let lDragger = ''; 9 | 10 | const dragger = { 11 | init(container, options) { 12 | return dragula([...container], options); 13 | }, 14 | 15 | cardDragger() { 16 | if (cDragger) { 17 | cDragger.destroy(); 18 | } 19 | cDragger = dragger.init( 20 | Array.from(document.querySelectorAll('.card-list')), 21 | ); 22 | 23 | cDragger.on('drop', async (el, wrapper, target, siblings) => { 24 | // list 이동과는 다르게, card 이동은 list간의 이동도 가능해야하기때문에 listId 를 줌. 25 | const listId = wrapper.getAttribute('listId') * 1; 26 | 27 | // 모듈화 28 | const { id, pos } = this.computePos('card', el, wrapper); 29 | 30 | try { 31 | await updateCardAPI(id, { listId, pos }); 32 | await store.dispatch('READ_BOARD_DETAIL', store.state.board.id); 33 | } catch (error) { 34 | console.log(error); 35 | alert('카드 순서가 업데이트 되지 않았습니다.'); 36 | } 37 | }); 38 | }, 39 | 40 | listDragger() { 41 | if (lDragger) { 42 | lDragger.destroy(); 43 | } 44 | const options = { 45 | invalid: (el, handle) => !/^list/.test(handle.className), 46 | // list는 가로 방향으로만 동작하므로 options에 direction을 넣어줘야 함. 47 | direction: 'horizontal', 48 | }; 49 | lDragger = dragger.init( 50 | Array.from(document.querySelectorAll('.list-section')), 51 | options, 52 | 'list', 53 | ); 54 | lDragger.on('drop', async (el, wrapper, target, siblings) => { 55 | // 모듈화 56 | const { id, pos } = this.computePos('list', el, wrapper); 57 | 58 | try { 59 | await updateListAPI(id, { pos }); 60 | await store.dispatch('READ_BOARD_DETAIL', store.state.board.id); 61 | } catch (error) { 62 | console.log(error); 63 | alert('리스트 순서가 업데이트 되지 않았습니다.'); 64 | } 65 | }); 66 | }, 67 | 68 | computePos(type, el, wrapper) { 69 | const id = el.getAttribute(`${type}Id`) * 1; 70 | let pos = 65535; 71 | const selectDom = type === 'list' ? '.list' : '.card-item'; 72 | 73 | const { prev, next } = this.siblings({ 74 | el, 75 | wrapper, 76 | candidates: Array.from(wrapper.querySelectorAll(selectDom)), 77 | type, 78 | }); 79 | 80 | if (!prev && next) { 81 | // 맨 앞으로 옮겼다면, 82 | pos = next.pos / 2; 83 | } else if (!next && prev) { 84 | // 맨 뒤로 옮겼다면, 85 | pos = prev.pos * 2; 86 | } else if (prev && next) { 87 | // 중간 어딘가로 옮겼다면, 88 | pos = (prev.pos + next.pos) / 2; 89 | } 90 | 91 | return { id, pos }; 92 | }, 93 | 94 | // candidates : 후보군 || type : card인지 list인지 구분 95 | siblings({ el, wrapper, candidates, type }) { 96 | const curId = el.getAttribute(`${type}Id`) * 1; 97 | 98 | let prev = null; 99 | let next = null; 100 | 101 | candidates.forEach((el, idx, arr) => { 102 | const id = el.getAttribute(`${type}Id`) * 1; 103 | if (id === curId) { 104 | // 이전 카드가 첫번째 카드가 아니라면 이전(-1)card의 id, pos를 줌 105 | prev = 106 | idx > 0 107 | ? { 108 | id: arr[idx - 1].getAttribute(`${type}Id`) * 1, 109 | pos: arr[idx - 1].getAttribute('pos') * 1, 110 | } 111 | : null; 112 | 113 | // 이후 카드가 마지막 카드가 아니라면 이후(+1)card의 id, pos를 줌 114 | next = 115 | idx < arr.length - 1 116 | ? { 117 | id: arr[idx + 1].getAttribute(`${type}Id`) * 1, 118 | pos: arr[idx + 1].getAttribute('pos') * 1, 119 | } 120 | : null; 121 | } 122 | }); 123 | return { prev, next }; 124 | }, 125 | }; 126 | 127 | export default dragger; 128 | -------------------------------------------------------------------------------- /src/components/auth/FindPassword.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 91 | 92 | 161 | -------------------------------------------------------------------------------- /src/components/common/header/CommonHeaderMenu.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 60 | 61 | 160 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/attachment/CardAttachmentList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 106 | 107 | 171 | -------------------------------------------------------------------------------- /src/components/board/boardHeader/BoardHeaderHashtagModal.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 141 | 142 | 181 | -------------------------------------------------------------------------------- /src/components/card/CardItem.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 143 | 144 | 192 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/checklists/CardChecklistWrapItem.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 130 | 131 | 194 | -------------------------------------------------------------------------------- /src/components/common/header/message/HeaderMessageModalConfirm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 147 | 148 | 187 | -------------------------------------------------------------------------------- /src/components/profile/PasswordChange.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 115 | 116 | 190 | -------------------------------------------------------------------------------- /src/components/mainPage/section/PersonalSection.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 123 | 124 | 183 | -------------------------------------------------------------------------------- /src/api/board.js: -------------------------------------------------------------------------------- 1 | import { board } from '@/api'; 2 | 3 | /** 4 | * @typedef {Object} Board 5 | * @property {string} userId - Board의 주인ID 6 | * @property {number} id - Board의 ID 7 | * @property {string} title - Board의 제목 8 | * @property {string} bgColor - 배경색상 9 | * @property {string[]} invitedUser - 초대된 유저 목록 10 | * @property {string} publicYn - 공개여부 11 | * @property {number[]} hashtag - 해시태크 12 | * @property {number} likeCount - 좋아요 카운트 13 | * @property {number} ownLike - 로그인 한 유저가 해당 board에 좋아요를 표시했는지 여부 14 | * @property {string} createdAt - 생성자 15 | * @property {string} createdBy - 생성날짜 16 | * @property {string} updatedAt - 수정자 17 | * @property {string} updatedBy - 수정날짜 18 | */ 19 | 20 | /** 21 | * @typedef {Object} BoardDetail 22 | * @property {List[]} lists - board가 가지고 있는 list 목록 23 | * @property {string} createdByPicture - 생성자 프로필 사진 경로 24 | * @property {number} id - Board의 ID 25 | * @property {string} title - Board의 제목 26 | * @property {string} bgColor - 배경색상 27 | * @property {string[]} invitedUser - 초대된 유저 목록 28 | * @property {string} publicYn - 공개여부 29 | * @property {number[]} hashtag - 해시태크 30 | * @property {number} likeCount - 좋아요 카운트 31 | * @property {number} ownLike - 로그인 한 유저가 해당 board에 좋아요를 표시했는지 여부 32 | * @property {string} createdAt - 생성자 33 | * @property {string} createdBy - 생성날짜 34 | */ 35 | 36 | /** 37 | * @typedef List 38 | * @property {Card[]} cards - list가 가지고 있는 card 목록 39 | * @property {number} boardId - list가 속해있는 Board의 ID 40 | * @property {number} id - list의 ID 41 | * @property {number} pos - Dragula에 사용되는 포지션 값 42 | * @property {string} title - list의 제목 43 | */ 44 | 45 | /** 46 | * @typedef {Object} Card 47 | * @property {string} description - 메모(설명) 48 | * @property {string} dueDate - 날짜 체크 값 49 | * @property {number} id - card의 ID 50 | * @property {number} pos - Dragula에 사용되는 포지션 값 51 | * @property {string} title - card의 제목 52 | * @property {string} isAttachment - 첨부파일 여부 53 | * @property {string} isChecklist - 체크리스트 여부 54 | * @property {string[]} labelColor - 레이블 색상 55 | * @property {string} listId - card가 속해있는 list의 ID 56 | * @property {string} location - 장소 위치 값 57 | */ 58 | 59 | /** 60 | * @typedef {Object} CreateBoardInfo 61 | * @property {string} title - 제목 62 | * @property {string} publicYn - 공개여부 63 | * @property {string[]} hashtag - 해시태그 64 | * @property {string} bgColor - 배경색상 65 | */ 66 | 67 | /** 68 | * @typedef {Object} UpdateBoardInfo 69 | * @property {string} title - 제목 70 | * @property {string} publicYn - 공개여부 71 | * @property {string[]} hashtag - 해시태그 72 | * @property {string} bgColor - 배경색상 73 | * @property {string[]} invitedUser - 초대된 유저 ID 정보 74 | */ 75 | 76 | /** 77 | * 로그인 한 유저의 Board 목록을 조회(Order by createdAt) 78 | * @param {string} lastCreatedAt - 마지막 Board Item DOM의 생성날짜. 79 | * @returns {Promise} 80 | */ 81 | const readPersonalBoardAPI = lastCreatedAt => 82 | board.get(`/personal/${lastCreatedAt}`); 83 | 84 | /** 85 | * 특정 유저의 Board 목록 조회 86 | * @param {string} searchUserId - 특정 유저의 ID 87 | * @param {lastCreatedAt} searchUser - 마지막 Board Item DOM의 생성날짜. 88 | * @returns {Promise} 89 | */ 90 | const readSearchUserBoardAPI = (searchUserId, lastCreatedAt) => 91 | // SpringBoot의 GetMapping에서 @PathVariable 때문에 payload(객체 전달) 불가 92 | board.get(`/${searchUserId}/${lastCreatedAt}`); 93 | 94 | /** 95 | * 로그인 한 유저(personal)의 Board 보드 목록 재조회. recentBoard와의 sync를 맞추기 위해 필요하다. 96 | * @param {number} count - 현재 뿌려진 Personal Board 갯수 97 | * @returns {Promise} 98 | */ 99 | const readPersonalBoardLimitCountAPI = count => board.get(`/rerender/${count}`); 100 | 101 | /** 102 | * 로그인 한 유저의 최근 본 Board 목록 조회. 103 | * @param {string} recentLists - 최근 본 Board 목록 id Array를 stringify함. 104 | * @returns {Promise} 105 | */ 106 | const readRecentBoardAPI = recentLists => board.get(`/recent/${recentLists}`); 107 | 108 | /** 109 | * 로그인 한 유저의 초대된 Board 목록 조회. 110 | * @param {string} invitedLists - 초대된 Board 목록 id Array를 stringify함. 111 | * @returns {Promise} 112 | */ 113 | const readInvitedBoardAPI = invitedLists => 114 | board.get(`/invited/${invitedLists}`); 115 | 116 | /** 117 | * Board 상세 페이지의 데이터를 모두 가져온다. 118 | * @param {number} boardId - 선택한 Board item의 ID 119 | * @returns {Promise} 120 | */ 121 | const readBoardDetailAPI = boardId => board.get(`detail/${boardId}`); 122 | 123 | /** 124 | * Board 생성 125 | * @param {CreateBoardInfo} createBoardInfo - Board 생성 정보 126 | * @returns {Promise} 127 | */ 128 | const createBoardAPI = createBoardInfo => board.post('/', createBoardInfo); 129 | 130 | /** 131 | * Board 수정 132 | * @param {UpdateBoardInfo} updateBoardInfo - Board 수정 정보 133 | * @returns {Promise} 134 | */ 135 | const updateBoardAPI = (id, updateBoardInfo) => 136 | board.put(`/${id}`, updateBoardInfo); 137 | 138 | /** 139 | * Board 삭제 140 | * @param {number} id - Board ID 141 | * @returns {Promise} - boardId 142 | */ 143 | const deleteBoardAPI = id => board.delete(`/${id}`); 144 | 145 | /** 146 | * 푸시 메세지를 승락할 시, 해당 Board의 정보를 조회 147 | * @param {number} boardId - 푸시메세지에 등록된 Board ID 148 | * @returns {Promise} 149 | */ 150 | const readBoardForAcceptMessageAPI = boardId => board.get(`/${boardId}`); 151 | 152 | export { 153 | readPersonalBoardAPI, 154 | readSearchUserBoardAPI, 155 | readPersonalBoardLimitCountAPI, 156 | readRecentBoardAPI, 157 | readInvitedBoardAPI, 158 | readBoardDetailAPI, 159 | createBoardAPI, 160 | updateBoardAPI, 161 | deleteBoardAPI, 162 | readBoardForAcceptMessageAPI, 163 | }; 164 | -------------------------------------------------------------------------------- /src/components/card/cardModal/subModal/SubModal.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 124 | 125 | 203 | -------------------------------------------------------------------------------- /src/components/profile/AboutUserSignout.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 138 | 139 | 209 | -------------------------------------------------------------------------------- /src/utils/validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} Result 3 | * @property {boolean} flag - 체크 결과 4 | * @property {string} message - 실패 이유 5 | */ 6 | 7 | /** 8 | * 길이 체크 함수 9 | * @param {string} value - 길이를 체크할 대상 10 | * @param {number} first - 사잇 값 중 첫 번째 길이.(만약 last 변수가 없다면 이하의 의미가 된다.) 11 | * @param {number} last - 사잇 값 중 두 번째 길이. 12 | * @returns {Result} 13 | */ 14 | const length = (value, first, last) => { 15 | const length = value.length; 16 | let flag = false; 17 | let message = ''; 18 | 19 | if (!last) { 20 | if (length < first) { 21 | flag = true; 22 | } else { 23 | message = `${first}자 이내로 입력해주세요.`; 24 | flag = false; 25 | } 26 | } else { 27 | if (length >= first && length <= last) { 28 | flag = true; 29 | } else { 30 | message = `${first} ~ ${last}자 사이로 입력해주세요.`; 31 | flag = false; 32 | } 33 | } 34 | return { flag, message }; 35 | }; 36 | 37 | /** 38 | * 공백 체크 함수 39 | * @param {string} value - 공백을 체크할 대상 40 | * @returns {Result} 41 | */ 42 | const blank = value => { 43 | let flag = false; 44 | let message = ''; 45 | 46 | if (value.search(/\s/) != -1) { 47 | flag = false; 48 | message = '공백을 제거해주세요.'; 49 | } else { 50 | flag = true; 51 | } 52 | 53 | return { flag, message }; 54 | }; 55 | 56 | /** 57 | * 비밀번호 정규화 체크 함수 58 | * @param {string} value - 비밀번호 체크 대상 59 | * @returns {Result} 60 | */ 61 | const password = value => { 62 | let flag = false; 63 | let message = ''; 64 | 65 | const num = value.search(/[0-9]/g); 66 | const eng = value.search(/[a-z]/gi); 67 | const spe = value.search(/[`~!@@#$%^&*|₩₩₩'₩";:₩/?]/gi); 68 | 69 | if (num < 0 || eng < 0 || spe < 0) { 70 | message = '비밀번호는 영문, 숫자, 특수문자를 혼합하여 입력해주세요.'; 71 | flag = false; 72 | } else { 73 | flag = true; 74 | } 75 | return { flag, message }; 76 | }; 77 | 78 | /** 79 | * 이메일 정규화 체크 함수 80 | * @param {string} value - 이메일 체크 대상 81 | * @returns {Result} 82 | */ 83 | const email = value => { 84 | let flag = false; 85 | let message = ''; 86 | 87 | const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 88 | if (re.test(String(value).toLowerCase())) { 89 | flag = true; 90 | } else { 91 | message = '이메일 유형에 맞지 않습니다.'; 92 | flag = false; 93 | } 94 | return { flag, message }; 95 | }; 96 | 97 | /** 98 | * 아이디 정규화 체크 함수 99 | * @param {string} value - 아이디 체크 대상 100 | * @returns {Result} 101 | */ 102 | const id = value => { 103 | let flag = false; 104 | let message = ''; 105 | 106 | const re = /^[a-z]+[a-z0-9]/g; 107 | if (re.test(String(value))) { 108 | flag = true; 109 | } else { 110 | message = '아이디는 영문자로 시작하는 영문자 또는 숫자이어야 합니다.'; 111 | flag = false; 112 | } 113 | return { flag, message }; 114 | }; 115 | 116 | /** 117 | * 비밀번호 재입력과 비밀번호가 같은지 체크하는 함수 118 | * @param {string} first - 원래 입력한 비밀번호 119 | * @param {string} second - 후에 입력한 재입력 비밀번호 120 | * @returns {Result} 121 | */ 122 | const passwordAgainCheck = (first, second) => { 123 | let flag = false; 124 | let message = ''; 125 | 126 | if (first === second) { 127 | flag = true; 128 | } else { 129 | message = '다시 입력한 비밀번호가 같지 않습니다.'; 130 | flag = false; 131 | } 132 | return { flag, message }; 133 | }; 134 | 135 | /** 136 | * currying을 구현할 함수. 137 | * @param {function} regFunc - 정규식 함수 138 | * @param {function} lengthFunc - 길이체크 함수 139 | * @param {...number} args - 길이체크 함수에 들어갈 매개변수들 140 | * @param {string} value - 최종적으로 체크할 값 141 | * @returns {Result} 142 | */ 143 | const valid = (regFunc, lengthFunc, ...args) => value => { 144 | // closure 구현. 함수 안의 값은 함수 종료 후에도 값을 기억하고 있다. 145 | const regObj = regFunc(value); 146 | const blankObj = blank(value); 147 | const lengthObj = lengthFunc(value, ...args); 148 | 149 | if (regObj.flag) { 150 | if (blankObj.flag) { 151 | return lengthObj; 152 | } else { 153 | return blankObj; 154 | } 155 | } else { 156 | return regObj; 157 | } 158 | }; 159 | 160 | const emailValidCheck = valid(email, length, 20); 161 | const passwordValidCheck = valid(password, length, 1, 20); 162 | const idValidCheck = valid(id, length, 4, 19); 163 | 164 | /** 165 | * 사용법 예시 166 | * emailValidCheck('pozafly@gamil.com'); 167 | * passwordValidCheck('1238447453ejfb'); 168 | * idValidCheck('pozafly'); 169 | */ 170 | export { 171 | emailValidCheck, 172 | passwordValidCheck, 173 | idValidCheck, 174 | passwordAgainCheck, 175 | }; 176 | 177 | // 아래는 클로저로 바꾸기 전 사용했던 함수들. 178 | 179 | // const validateId = id => { 180 | // const re = /^[a-z]+[a-z0-9]{5,19}$/g; 181 | // return re.test(String(id)); 182 | // }; 183 | 184 | // const validatePw = pw => { 185 | // const num = pw.search(/[0-9]/g); 186 | // const eng = pw.search(/[a-z]/gi); 187 | // const spe = pw.search(/[`~!@@#$%^&*|₩₩₩'₩";:₩/?]/gi); 188 | // let message = ''; 189 | 190 | // if (pw.length < 5 || pw.length > 20) { 191 | // message = '비밀번호는 5자리 ~ 20자리 이내로 입력해주세요.'; 192 | // return [false, message]; 193 | // } else if (pw.search(/\s/) != -1) { 194 | // message = '비밀번호는 공백 없이 입력해주세요.'; 195 | // return [false, message]; 196 | // } else if (num < 0 || eng < 0 || spe < 0) { 197 | // message = '비밀번호는 영문,숫자, 특수문자를 혼합하여 입력해주세요.'; 198 | // return [false, message]; 199 | // } else { 200 | // return [true, '']; 201 | // } 202 | // }; 203 | 204 | // const validateEmail = email => { 205 | // const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 206 | // return re.test(String(email).toLowerCase()); 207 | // }; 208 | 209 | // export { validateId, validatePw, validateEmail }; 210 | -------------------------------------------------------------------------------- /src/components/card/cardModal/mainModal/location/CardLocation.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 122 | 123 | 200 | --------------------------------------------------------------------------------