├── .firebaserc ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test-deploy.yml ├── .gitignore ├── README.md ├── firebase.json ├── functions ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── api │ ├── index.js │ └── routes │ │ ├── auth │ │ ├── authDoorbellGET.js │ │ ├── authSignOutPOST.js │ │ ├── authSignUpPOST.js │ │ ├── authTestGET.js │ │ ├── authUserDELETE.js │ │ └── index.js │ │ ├── feed │ │ ├── feedGET.js │ │ ├── feedLikePOST.js │ │ ├── feedReportPOST.js │ │ └── index.js │ │ ├── index.js │ │ ├── myroom │ │ ├── index.js │ │ ├── myroomGET.js │ │ ├── myroomRoomGET.js │ │ ├── myroomThumbnailListGET.js │ │ └── myroomThumbnailPATCH.js │ │ ├── notice │ │ ├── active │ │ │ ├── activeGET.js │ │ │ ├── activeReadPATCH.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── noticeDELETE.js │ │ ├── noticeNewGET.js │ │ ├── noticeSettingGET.js │ │ ├── noticeSettingPATCH.js │ │ └── service │ │ │ ├── index.js │ │ │ ├── serviceGET.js │ │ │ └── serviceReadPATCH.js │ │ ├── room │ │ ├── index.js │ │ ├── roomCodeGET.js │ │ ├── roomDELETE.js │ │ ├── roomDetailGET.js │ │ ├── roomEnterPOST.js │ │ ├── roomListGET.js │ │ ├── roomOutDELETE.js │ │ ├── roomPOST.js │ │ ├── roomPurposePATCH.js │ │ ├── roomReadPATCH.js │ │ ├── roomRecordPOST.js │ │ ├── roomStartPOST.js │ │ ├── roomStatusPOST.js │ │ ├── roomTimelineGET.js │ │ ├── roomWaitingGET.js │ │ ├── roomWaitingMemberGET.js │ │ └── sparkPOST.js │ │ ├── scheduling │ │ ├── index.js │ │ ├── inspectPOST.js │ │ └── remindPOST.js │ │ ├── user │ │ ├── index.js │ │ ├── userProfileGET.js │ │ └── userProfilePATCH.js │ │ └── version │ │ ├── index.js │ │ ├── versionGET.js │ │ └── versionPATCH.js ├── config │ ├── dbConfig.js │ └── firebaseClient.js ├── constants │ ├── alarmMessage.js │ ├── day.js │ ├── defaultProfileImg.js │ ├── jwt.js │ ├── lifeTimelineMessage.js │ ├── responseMessage.js │ ├── statusCode.js │ └── termList.js ├── db │ ├── db.js │ ├── dialog.js │ ├── index.js │ ├── lifeTimeline.js │ ├── like.js │ ├── notice.js │ ├── ownership.js │ ├── record.js │ ├── remind.js │ ├── report.js │ ├── room.js │ ├── schedule.js │ ├── spark.js │ ├── user.js │ └── version.js ├── index.js ├── lib │ ├── convertSnakeToCamel.js │ ├── deleteImage.js │ ├── jwtHandlers.js │ ├── passedDayToStr.js │ ├── pushAlarm.js │ └── util.js ├── middlewares │ ├── auth.js │ ├── slackAPI.js │ └── uploadImage.js ├── package.json └── scheduler │ └── funcs.js └── package.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "we-sopt-spark" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## 📌 Issue 3 | 4 | - Issue ! 5 | 6 | ## 📝 To-do 7 | 8 | - [ ] todo! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 📌 Issue 11 | 12 | 13 | ## 📝 To-do 14 | 15 | - [ ] todo! 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### ✅ Default Checklist 2 | 3 | - [x] check branch 4 | 5 | - [x] set Labels 6 | 7 | - [x] set Reviewers 8 | 9 | --- 10 | 11 | ### 📕 Task 12 | 13 | - [x] Something you did.. 14 | 15 | --- 16 | 17 | ### 😳 Issue 18 | 19 | - [ ] Help me.. -------------------------------------------------------------------------------- /.github/workflows/test-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Spark Test Server Deploy 2 | on: 3 | push: 4 | branches: [test] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 12 | - uses: actions/checkout@v2 13 | 14 | # Create firebase-sdk file 15 | - name: Create Firebase SDK File 16 | id: create-json 17 | uses: jsdaniell/create-json@1.1.2 18 | with: 19 | name: "we-sopt-spark-firebase-adminsdk-emnjd-30d5170309.json" 20 | json: ${{ secrets.FIREBASE_SDK }} 21 | dir: "functions/" 22 | 23 | # Create .env file 24 | - name: Create .env 25 | run: | 26 | touch ./functions/.env 27 | echo "${{ secrets.DOT_ENV }}" > ./functions/.env 28 | shell: bash 29 | 30 | # Update .firebaserc file 31 | - name: Update .firebaserc 32 | run: | 33 | rm .firebaserc 34 | touch .firebaserc 35 | echo "${{ secrets.FIREBASE_RC }}" > .firebaserc 36 | shell: bash 37 | 38 | # Replace firebaseClient Config file 39 | - name: Replace FirebaseClient Config file 40 | run: | 41 | rm ./functions/config/firebaseClient.js 42 | touch ./functions/config/firebaseClient.js 43 | echo "${{ secrets.TEST_SERVER_FIREBASE_CLIENT }}" > ./functions/config/firebaseClient.js 44 | shell: bash 45 | 46 | - name: Install npm packages 47 | run: | 48 | cd functions 49 | npm install 50 | 51 | - name: Deploy to Firebase 52 | uses: w9jds/firebase-action@master 53 | with: 54 | args: deploy --only functions --project spark-test-server 55 | env: 56 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | 69 | # Created by https://www.toptal.com/developers/gitignore/api/node 70 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 71 | 72 | ### Node ### 73 | # Logs 74 | logs 75 | *.log 76 | npm-debug.log* 77 | yarn-debug.log* 78 | yarn-error.log* 79 | lerna-debug.log* 80 | .pnpm-debug.log* 81 | 82 | # Diagnostic reports (https://nodejs.org/api/report.html) 83 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 84 | 85 | # Runtime data 86 | pids 87 | *.pid 88 | *.seed 89 | *.pid.lock 90 | 91 | # Directory for instrumented libs generated by jscoverage/JSCover 92 | lib-cov 93 | 94 | # Coverage directory used by tools like istanbul 95 | coverage 96 | *.lcov 97 | 98 | # nyc test coverage 99 | .nyc_output 100 | 101 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 102 | .grunt 103 | 104 | # Bower dependency directory (https://bower.io/) 105 | bower_components 106 | 107 | # node-waf configuration 108 | .lock-wscript 109 | 110 | # Compiled binary addons (https://nodejs.org/api/addons.html) 111 | build/Release 112 | 113 | # Dependency directories 114 | node_modules/ 115 | jspm_packages/ 116 | 117 | # Snowpack dependency directory (https://snowpack.dev/) 118 | web_modules/ 119 | 120 | # TypeScript cache 121 | *.tsbuildinfo 122 | 123 | # Optional npm cache directory 124 | .npm 125 | 126 | # Optional eslint cache 127 | .eslintcache 128 | 129 | # Microbundle cache 130 | .rpt2_cache/ 131 | .rts2_cache_cjs/ 132 | .rts2_cache_es/ 133 | .rts2_cache_umd/ 134 | 135 | # Optional REPL history 136 | .node_repl_history 137 | 138 | # Output of 'npm pack' 139 | *.tgz 140 | 141 | # Yarn Integrity file 142 | .yarn-integrity 143 | 144 | # dotenv environment variables file 145 | .env 146 | .env.test 147 | .env.production 148 | 149 | # parcel-bundler cache (https://parceljs.org/) 150 | .cache 151 | .parcel-cache 152 | 153 | # Next.js build output 154 | .next 155 | out 156 | 157 | # Nuxt.js build / generate output 158 | .nuxt 159 | dist 160 | 161 | # Gatsby files 162 | .cache/ 163 | # Comment in the public line in if your project uses Gatsby and not Next.js 164 | # https://nextjs.org/blog/next-9-1#public-directory-support 165 | # public 166 | 167 | # vuepress build output 168 | .vuepress/dist 169 | 170 | # Serverless directories 171 | .serverless/ 172 | 173 | # FuseBox cache 174 | .fusebox/ 175 | 176 | # DynamoDB Local files 177 | .dynamodb/ 178 | 179 | # TernJS port file 180 | .tern-port 181 | 182 | # Stores VSCode versions used for testing VSCode extensions 183 | .vscode-test 184 | 185 | # yarn v2 186 | .yarn/cache 187 | .yarn/unplugged 188 | .yarn/build-state.yml 189 | .yarn/install-state.gz 190 | .pnp.* 191 | 192 | ### Node Patch ### 193 | # Serverless Webpack directories 194 | .webpack/ 195 | 196 | # End of https://www.toptal.com/developers/gitignore/api/node 197 | 198 | .DS_Store 199 | 200 | # Firebase service account 201 | we-sopt-spark-firebase-adminsdk-emnjd-30d5170309.json 202 | 203 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🎇 Spark,  A large fire comes from small spark 2 | 3 | > 👑 WE SOPT 29th APPJAM 대상 👑
4 | > 친구와 함께하는 66일간의 습관 형성 서비스!
5 | > 프로젝트 기간 : 2022. 01. 03. ~ 2022. 01. 22.
6 | > 📲 [App Store](https://apps.apple.com/kr/app/spark-%EC%8A%A4%ED%8C%8C%ED%81%AC-%EC%B9%9C%EA%B5%AC%EC%99%80-%EC%8A%B5%EA%B4%80-%EA%B4%80%EB%A6%AC/id1605811861) | [Play Store](https://play.google.com/store/apps/details?id=com.teamsparker.android) 7 | 8 | ## 🔥 Branding 9 | 10 | ![banner](https://user-images.githubusercontent.com/39653584/150547366-5ff166d7-874d-4b4d-a507-7a387c348991.png) 11 | 12 | ![영권쌤 거](https://user-images.githubusercontent.com/39653584/150317112-aa048eba-ee94-4711-99dd-89dcb3c2746e.png) 13 | 14 | 18 | 19 | --- 20 | 21 | ## 📚 API Document 22 | 23 | ###   [Spark API Document ✨](https://www.notion.so/API-94b97e62a8b84769a784d86992287119) 24 | 25 | | Function | Description | Developer | 26 | | :---: | --- | --- | 27 | | Auth | Apple, Kakao 로그인 | 🦋 영권 | 28 | | | Apple, Kakao 회원가입 | 🐱 설희 | 29 | | | 회원 탈퇴 | 🐱 설희 | 30 | | Feed | 피드 모아보기 | 🐱 설희 | 31 | | | 피드 좋아요 및 좋아요 취소 | 🐻‍❄️ 정현 / 🦋 영권 | 32 | | | 피드 신고하기 | 🐱 설희 | 33 | | Room | 습관방 생성 | 🦋 영권 | 34 | | | 방 리스트 조회 | 🐱 설희 | 35 | | | 대기방 상세 조회 | 🦋 영권 | 36 | | | 습관방 상세 조회 | 🐱 설희 | 37 | | | 나의 목표 설정하기 | 🦋 영권 | 38 | | | 습관방 시작 | 🦋 영권 | 39 | | | 대기방 인원 조회 | 🦋 영권 | 40 | | | 쉴래요, 고민중 | 🦋 영권 | 41 | | | 습관 인증하기 | 🐱 설희 | 42 | | | 스파크 보내기 | 🐱 설희 | 43 | | | 참여 코드로 대기방 조회 | 🦋 영권 | 44 | | | 습관방 참여 | 🦋 영권 | 45 | | | 참여자 내보내기 | 🦋 영권 | 46 | | | 습관방 및 대기방 나가기 | 🦋 영권 | 47 | | | 대기방 삭제 | 🦋 영권 | 48 | | | 홈에서 성공 및 실패한 카드 | 🐱 설희 | 49 | | User | 유저 프로필 조회 | 🦋 영권 | 50 | | | 유저 프로필 수정 | 🦋 영권 | 51 | | Myroom | 보관함 리스트 불러오기 | 🐱 설희 | 52 | | | 특정 습관방 인증사진 모아보기 | 🐱 설희 | 53 | | | 보관함 대표 이미지 변경 | 🐱 설희 | 54 | | | 보관함 대표 이미지 목록 불러오기 | 🦋 영권 | 55 | | Notice | 서비스 알림 조회 | 🦋 영권 | 56 | | | 서비스 알림 읽음 처리 | 🦋 영권 | 57 | | | 활동 알림 조회 | 🦋 영권 | 58 | | | 활동 알림 읽음 처리 | 🦋 영권 | 59 | | | 알림 삭제 | 🦋 영권 | 60 | | | 새로운 알림 빨콩 조회 | 🦋 영권 | 61 | | Life Timeline | 생명 타임라인 조회 | 🦋 영권 | 62 | | FCM | 푸시알림 전송 기능 | 🦋 영권 | 63 | | | 푸시알림 ON / OFF Toggle | 🦋 영권 | 64 | | | 푸시알림 설정 조회 | 🦋 영권 | 65 | | Version | 최신 버전정보 불러오기 | 🦋 영권 | 66 | | | 최신 버전정보 갱신하기 | 🦋 영권 | 67 | 68 | 69 | --- 70 | 71 | 309 | 310 | 311 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint" 5 | ], 6 | "source": "functions" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: ["eslint:recommended", "eslint-config-prettier"], 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | }, 11 | rules: { 12 | "no-prototype-builtins": "off", 13 | "no-self-assign": "off", 14 | "no-empty": "off", 15 | "no-case-declarations": "off", 16 | "consistent-return": "off", 17 | "arrow-body-style": "off", 18 | camelcase: "off", 19 | quotes: "off", 20 | "no-unused-vars": "off", 21 | "comma-dangle": "off", 22 | "no-bitwise": "off", 23 | "no-use-before-define": "off", 24 | "no-extra-boolean-cast": "off", 25 | "no-empty-pattern": "off", 26 | curly: "off", 27 | "no-unreachable": "off", 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: "all", 6 | arrowParens: "always", 7 | printWidth: 200, 8 | tabWidth: 2, 9 | }; 10 | -------------------------------------------------------------------------------- /functions/api/index.js: -------------------------------------------------------------------------------- 1 | // 각종 모듈들 2 | const functions = require('firebase-functions'); 3 | const express = require('express'); 4 | const cors = require('cors'); 5 | const cookieParser = require('cookie-parser'); 6 | const dotenv = require('dotenv'); 7 | const hpp = require('hpp'); 8 | const helmet = require('helmet'); 9 | 10 | // 보안 상 깃허브에 올리면 안 되는 정보를 .env라는 파일로 관리하기 위해 사용하는 모듈 11 | dotenv.config(); 12 | 13 | // initializing 14 | const app = express(); 15 | 16 | // Cross-Origin Resource Sharing을 열어주는 미들웨어 17 | // https://evan-moon.github.io/2020/05/21/about-cors/ 에서 자세한 정보 확인 18 | app.use(cors()); 19 | 20 | // 보안을 위한 미들웨어들 21 | // process.env.NODE_ENV는 배포된 서버에서는 'production'으로, 로컬에서 돌아가는 서버에서는 'development'로 고정됨. 22 | if (process.env.NODE_ENV === 'production') { 23 | app.use(hpp()); 24 | app.use(helmet()); 25 | } 26 | 27 | // request에 담긴 정보를 json 형태로 파싱하기 위한 미들웨어들 28 | app.use(express.json()); 29 | app.use(express.urlencoded({ extended: true })); 30 | app.use(cookieParser()); 31 | 32 | // 라우팅: routes 폴더로 관리 33 | app.use('/', require('./routes')); 34 | 35 | // route 폴더에 우리가 지정할 경로가 아닌 다른 경로로 요청이 올 경우, 36 | // 잘못된 경로로 요청이 들어왔다는 메시지를 클라이언트에 보냄 37 | app.use('*', (req, res) => { 38 | res.status(404).json({ 39 | status: 404, 40 | success: false, 41 | message: '요청 경로가 올바르지 않습니다', 42 | }); 43 | }); 44 | 45 | // express를 firebase functions로 감싸주는 코드 46 | module.exports = functions 47 | .runWith({ 48 | timeoutSeconds: 300, // 요청을 처리하는 과정이 300초를 초과하면 타임아웃 시키기 49 | memory: '512MB', // 서버에 할당되는 메모리 50 | }) 51 | .region('asia-northeast3') // 서버가 돌아갈 region. asia-northeast3는 서울 52 | .https.onRequest(async (req, res) => { 53 | // 들어오는 요청에 대한 로그를 콘솔에 찍기. 디버깅 때 유용하게 쓰일 예정. 54 | // 콘솔에 찍고 싶은 내용을 원하는 대로 추가하면 됨. (req.headers, req.query 등) 55 | console.log('\n\n', '[api]', `[${req.method.toUpperCase()}]`, req.originalUrl, req.body); 56 | 57 | // 맨 위에 선언된 express app 객체를 리턴. 58 | // 요것이 functions/index.js 안의 api: require("./api")에 들어가는 것. 59 | return app(req, res); 60 | }); 61 | -------------------------------------------------------------------------------- /functions/api/routes/auth/authDoorbellGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const jwtHandlers = require('../../../lib/jwtHandlers'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const { userDB } = require('../../../db'); 9 | 10 | /** 11 | * @회원가입_로그인_분기처리 12 | * @route GET /auth/doorbell?socialId= 13 | * @error 14 | * 1. socialId 또는 fcmToken 누락 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const { socialId, fcmToken, os } = req.query; 19 | 20 | // @error 1. socialId 또는 fcmToken 누락 21 | if (!socialId || !fcmToken) { 22 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 23 | } 24 | 25 | let client; 26 | 27 | try { 28 | client = await db.connect(); 29 | 30 | const user = await userDB.getUserBySocialId(client, socialId); 31 | 32 | // 회원가입 한적 없는 사용자 33 | if (!user) { 34 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.NOT_SIGNED_UP, { isNew: true })); 35 | } 36 | 37 | let data = jwtHandlers.sign(user); 38 | data.isNew = false; 39 | 40 | if (!os) { 41 | await userDB.updateDeviceTokenById(client, user.userId, fcmToken); 42 | } else { 43 | await userDB.updateDeviceTokenAndOsById(client, user.userId, fcmToken, os); 44 | } 45 | 46 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.ALREADY_SIGNED_UP, data)); 47 | } catch (error) { 48 | console.log(error); 49 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 50 | const slackMessage = `[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 51 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 52 | 53 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 54 | } finally { 55 | client.release(); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /functions/api/routes/auth/authSignOutPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { userDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | 9 | /** 10 | * @로그아웃 11 | * @route POST /auth/signout 12 | * @error 13 | */ 14 | 15 | module.exports = async (req, res) => { 16 | const user = req.user; 17 | 18 | let client; 19 | 20 | try { 21 | client = await db.connect(); 22 | 23 | await userDB.emptyDeviceTokenById(client, user.userId); 24 | 25 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.LOGOUT_SUCCESS)); 26 | } catch (error) { 27 | console.log(error); 28 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 29 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 30 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 31 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 32 | } finally { 33 | client.release(); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /functions/api/routes/auth/authSignUpPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { userDB, ownershipDB } = require('../../../db'); 7 | const { DEFAULT_PROFILE_IMG_URL } = require('../../../constants/defaultProfileImg'); 8 | const jwtHandlers = require('../../../lib/jwtHandlers'); 9 | const slackAPI = require('../../../middlewares/slackAPI'); 10 | 11 | /** 12 | * @회원가입 13 | * @route POST /auth/signup 14 | * @body socialId:string, nickname:string, profileImg:file 15 | * @error 16 | * 1. socialId/nickname이 전달되지 않음 17 | * 2. 이미 존재하는 socialId 18 | * 3. 닉네임 10자 초과 19 | */ 20 | 21 | module.exports = async (req, res) => { 22 | const { socialId, nickname, fcmToken } = req.body; 23 | 24 | // @error 1. socialId/nickname이 전달되지 않음 25 | if (!socialId || !nickname || !fcmToken) { 26 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 27 | } 28 | // @error 3. 닉네임 10자 초과 29 | if (nickname.length > 10) { 30 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.TOO_LONG_NICKNAME)); 31 | } 32 | let client; 33 | try { 34 | client = await db.connect(); 35 | 36 | // @error 2. 이미 존재하는 socialIds 37 | const alreaySocialId = await userDB.getUserBySocialId(client, socialId); 38 | 39 | if (alreaySocialId) { 40 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ALREADY_SOCIALID)); 41 | } 42 | 43 | let profileImg = req.imageUrls; 44 | 45 | // 프로필 이미지가 넘어오지 않았으면, 기본 이미지로 설정 46 | if (profileImg.length === 0) { 47 | profileImg.push(DEFAULT_PROFILE_IMG_URL); 48 | } 49 | 50 | const user = await userDB.addUser(client, socialId, nickname, profileImg[0], fcmToken); 51 | 52 | // 프로필 이미지를 등록한 사용자라면 프로필 사진에 대한 ownership 부여 53 | if (user.profileImg !== DEFAULT_PROFILE_IMG_URL) { 54 | const profileUrl = user.profileImg; 55 | const profilePath = profileUrl.split('/')[profileUrl.split('/').length - 1].split('?')[0].replace('%2F', '/'); 56 | await ownershipDB.insertOwnership(client, user.userId, profilePath); 57 | } 58 | 59 | const { accesstoken } = jwtHandlers.sign(user); 60 | 61 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.CREATED_USER, { user, accesstoken })); 62 | } catch (error) { 63 | console.log(error); 64 | functions.logger.error(`[SIGNUP ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] socialId:${socialId} ${error}`); 65 | const slackMessage = `[SIGNUP ERROR] [${req.method.toUpperCase()}] ${req.originalUrl} [CONTENT] socialId:${socialId} ${error} ${JSON.stringify(error)}`; 66 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 67 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 68 | } finally { 69 | client.release(); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /functions/api/routes/auth/authTestGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | const util = require('../../../lib/util'); 4 | const statusCode = require('../../../constants/statusCode'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const { userDB } = require('../../../db'); 8 | const jwtHandlers = require('../../../lib/jwtHandlers'); 9 | 10 | 11 | module.exports = async (req, res) => { 12 | const user = req.user; 13 | console.log(user); 14 | if (!user) return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_USER)); 15 | 16 | let client; 17 | 18 | try { 19 | client = await db.connect(); 20 | 21 | res.status(statusCode.OK).send(util.success(statusCode.OK, "token -> user 활용법", user)); 22 | } catch (error) { 23 | console.log(error); 24 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 25 | 26 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 27 | } finally { 28 | client.release(); 29 | } 30 | }; -------------------------------------------------------------------------------- /functions/api/routes/auth/authUserDELETE.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { userDB, roomDB } = require('../../../db'); 7 | const { DEFAULT_PROFILE_IMG_URL } = require('../../../constants/defaultProfileImg'); 8 | const jwtHandlers = require('../../../lib/jwtHandlers'); 9 | const slackAPI = require('../../../middlewares/slackAPI'); 10 | 11 | /** 12 | * @회원_탈퇴 13 | * @route DELETE /auth/user 14 | */ 15 | 16 | module.exports = async (req, res) => { 17 | const user = req.user; 18 | let client; 19 | 20 | try { 21 | client = await db.connect(); 22 | 23 | const deletedUser = await userDB.deleteUserSoft(client, user.userId, user.socialId); 24 | await roomDB.outByUserId(client, user.userId); 25 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.DELETE_USER_SUCCESS)); 26 | } catch (error) { 27 | console.log(error); 28 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 29 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 30 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 31 | } finally { 32 | client.release(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /functions/api/routes/auth/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const uploadImageIntoSubDir = require('../../../middlewares/uploadImage'); 4 | const { checkUser } = require('../../../middlewares/auth'); 5 | 6 | router.post('/signup', uploadImageIntoSubDir('users'), require('./authSignUpPOST')); 7 | router.post('/signout', checkUser, require('./authSignOutPOST')); 8 | router.get('/doorbell', require('./authDoorbellGET')); 9 | router.delete('/user', checkUser, require('./authUserDELETE')); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /functions/api/routes/feed/feedGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const convertDay = require('../../../constants/day'); 6 | const db = require('../../../db/db'); 7 | const { roomDB, sparkDB, likeDB } = require('../../../db'); 8 | const slackAPI = require('../../../middlewares/slackAPI'); 9 | const dayjs = require('dayjs'); 10 | const _ = require('lodash'); 11 | 12 | /** 13 | * @피드_조회 14 | * @route GET /feed?lastId=&size= 15 | * @error 16 | * 1. 잘못된 lastId 17 | */ 18 | 19 | module.exports = async (req, res) => { 20 | const lastId = Number(req.query.lastId); 21 | const size = Number(req.query.size); 22 | const user = req.user; 23 | 24 | let client; 25 | 26 | try { 27 | client = await db.connect(req); 28 | const rawRooms = await roomDB.getRoomsIncludingFailByUserId(client, user.userId); 29 | 30 | if (!rawRooms.length) { 31 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_FEED_SUCCESS, { records: [] })); 32 | } 33 | const roomIds = [...new Set(rawRooms.filter(Boolean).map((room) => room.roomId))]; 34 | const allRecords = await roomDB.getFeedRecordsByRoomIds(client, roomIds); 35 | const allRecordIds = allRecords.map((record) => record.recordId); 36 | 37 | let responseRecords = []; 38 | let recordIds = []; 39 | // 최초 요청이 아닐시 40 | if (lastId !== -1) { 41 | const lastIndex = _.indexOf(allRecordIds, lastId); 42 | // @error 1. 잘못된 last id 43 | if (lastIndex === -1) { 44 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.INVALID_LASTID)); 45 | } 46 | responseRecords = allRecords.slice(lastIndex + 1, lastIndex + 1 + size); 47 | recordIds = allRecordIds.slice(lastIndex + 1, lastIndex + 1 + size); 48 | } 49 | // 최초 요청시 50 | else { 51 | responseRecords = allRecords.slice(0, size); 52 | recordIds = allRecordIds.slice(0, size); 53 | } 54 | if (!recordIds.length) { 55 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_FEED_SUCCESS, { records: [] })); 56 | } 57 | let likeNums = []; 58 | let sparkNums = []; 59 | let isLikes = []; 60 | for (let i = 0; i < recordIds.length; i++) { 61 | const like = await likeDB.countLikeByRecordId(client, recordIds[i]); 62 | const isLike = await likeDB.checkIsLike(client, recordIds[i], user.userId); 63 | likeNums.push(Number(like[0].count)); 64 | if (isLike) { 65 | isLikes.push(true); 66 | } else { 67 | isLikes.push(false); 68 | } 69 | const spark = await sparkDB.countSparkByRecordId(client, recordIds[i]); 70 | sparkNums.push(Number(spark.count)); 71 | } 72 | 73 | let records = []; 74 | for (let i = 0; i < recordIds.length; i++) { 75 | const date = dayjs(responseRecords[i].date).format('YYYY-M-D'); 76 | const day = convertDay.numToString[dayjs(responseRecords[i].date).day()]; 77 | const roomName = _.find(rawRooms, { roomId: responseRecords[i].roomId }).roomName; 78 | records.push({ 79 | date, 80 | day, 81 | userId: responseRecords[i].userId, 82 | recordId: recordIds[i], 83 | nickname: responseRecords[i].nickname, 84 | profileImg: responseRecords[i].profileImg, 85 | roomName, 86 | certifyingImg: responseRecords[i].certifyingImg, 87 | likeNum: likeNums[i], 88 | sparkCount: sparkNums[i], 89 | isLiked: isLikes[i], 90 | timerRecord: responseRecords[i].timerRecord, 91 | isMyRecord: responseRecords[i].userId === user.userId ? true : false, 92 | }); 93 | } 94 | 95 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_FEED_SUCCESS, { records })); 96 | } catch (error) { 97 | console.log(error); 98 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 99 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 100 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 101 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 102 | } finally { 103 | client.release(); 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /functions/api/routes/feed/feedLikePOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const { roomDB, recordDB, likeDB, noticeDB } = require('../../../db'); 9 | 10 | /** 11 | * @피드_좋아요_및_취소 12 | * @route POST /feed/:recordId/like 13 | * @body 14 | * @error 15 | * 1. 유효하지 않은 recordId 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | const { recordId } = req.params; 20 | const user = req.user; 21 | const userId = user.userId; 22 | 23 | let client; 24 | 25 | try { 26 | client = await db.connect(req); 27 | 28 | const record = await recordDB.getRecordById(client, recordId); 29 | const entry = await roomDB.getEntryById(client, record.entryId); 30 | const room = await roomDB.getRoomById(client, entry.roomId); 31 | const { title, body, isService } = alarmMessage.FEED_LIKE(user.nickname, room.roomName); 32 | 33 | // @error 1. 유효하지 않은 recordId 34 | if (!record) { 35 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.RECORD_ID_NOT_VALID)); 36 | } 37 | 38 | const isLike = await likeDB.checkIsLike(client, recordId, userId); 39 | 40 | // Like 41 | if (!isLike) { 42 | await likeDB.sendLike(client, recordId, userId); 43 | 44 | // 본인이 본인의 게시물을 좋아할 경우 알림을 생성하지 않음 45 | if (userId !== entry.userId) { 46 | await noticeDB.addNotification(client, title, body, record.certifyingImg, entry.userId, isService, false, room.roomId); 47 | } 48 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SEND_LIKE_SUCCESS)); 49 | } 50 | 51 | // Unlike 52 | await likeDB.cancelLike(client, recordId, userId); 53 | await noticeDB.deleteNoticeByContentReceiverAndThumbnail(client, title, body, isService, entry.userId, record.certifyingImg); 54 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.CANCEL_LIKE_SUCCESS)); 55 | } catch (error) { 56 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 57 | console.log(error); 58 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 59 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 60 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 61 | } finally { 62 | client.release(); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /functions/api/routes/feed/feedReportPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { recordDB, likeDB, roomDB, reportDB } = require('../../../db'); 8 | const dayjs = require('dayjs'); 9 | 10 | /** 11 | * @피드_신고 12 | * @route POST /feed/:recordId/report 13 | * @body 14 | * @error 15 | * 1. reportReason이 전달되지 않음 16 | * 2. 유효하지 않은 recordId 17 | */ 18 | 19 | module.exports = async (req, res) => { 20 | const { recordId } = req.params; 21 | const { reportReason } = req.body; 22 | const user = req.user; 23 | 24 | let client; 25 | 26 | try { 27 | client = await db.connect(req); 28 | 29 | // @error 1. reportReason이 전달되지 않음 30 | if (!reportReason) { 31 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 32 | } 33 | 34 | const record = await recordDB.getRecordById(client, recordId); 35 | 36 | // @error 2. 유효하지 않은 recordId 37 | if (!record) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.RECORD_ID_NOT_VALID)); 39 | } 40 | 41 | const entry = await roomDB.getUserInfoByEntryId(client, record.entryId); 42 | 43 | await reportDB.addReport(client, user.userId, entry.userId, recordId); 44 | const reportData = ` 45 | 신고한 유저 ID: ${user.userId} 46 | 신고한 유저 닉네임: ${user.nickname} 47 | 신고대상 유저 ID: ${entry.userId} 48 | 신고대상 유저 닉네임: ${entry.nickname} 49 | 신고대상 record ID: ${record.recordId} 50 | 신고대상 record 사진: ${record.certifyingImg} 51 | 신고사유: ${reportReason} 52 | 신고일자: ${dayjs().add(9, 'hour')} 53 | `; 54 | slackAPI.feedReporotToSlack(reportData, slackAPI.DEV_WEB_HOOK_FEED_REPORT); 55 | 56 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.REPORT_FEED_SUCCESS)); 57 | } catch (error) { 58 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 59 | console.log(error); 60 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 61 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 62 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 63 | } finally { 64 | client.release(); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /functions/api/routes/feed/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../middlewares/auth'); 4 | 5 | router.get('', checkUser, require('./feedGET')); 6 | router.post('/:recordId/like', checkUser, require('./feedLikePOST')); 7 | router.post('/:recordId/report', checkUser, require('./feedReportPOST')); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /functions/api/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | router.use('/auth', require('./auth')); 5 | router.use('/feed', require('./feed')); 6 | router.use('/myroom', require('./myroom')); 7 | router.use('/notice', require('./notice')); 8 | router.use('/room', require('./room')); 9 | router.use('/user', require('./user')); 10 | router.use('/scheduling', require('./scheduling')); 11 | router.use('/version', require('./version')); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /functions/api/routes/myroom/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../middlewares/auth'); 4 | 5 | router.get('/:roomId', checkUser, require('./myroomRoomGET')); 6 | router.get('', checkUser, require('./myroomGET')); 7 | router.get('/thumbnail/:roomId', checkUser, require('./myroomThumbnailListGET')); 8 | router.patch('/:roomId/thumbnail/:recordId', checkUser, require('./myroomThumbnailPATCH')); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /functions/api/routes/myroom/myroomGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, sparkDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const dayjs = require('dayjs'); 9 | const _ = require('lodash'); 10 | 11 | /** 12 | * @보관함_리스트_불러오기 13 | * @route GET /myroom/:roomType?lastId=&size= 14 | * @error 15 | * 1. 잘못된 roomType 16 | * 2. 잘못된 lastId 17 | */ 18 | 19 | module.exports = async (req, res) => { 20 | const lastId = Number(req.query.lastId); 21 | const size = Number(req.query.size); 22 | const roomType = req.query.type; 23 | const user = req.user; 24 | 25 | let client; 26 | 27 | // @error 1. 잘못된 roomType 28 | if (!(roomType === 'ONGOING' || roomType === 'COMPLETE' || roomType === 'FAIL')) { 29 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.OUT_OF_VALUE)); 30 | } 31 | 32 | try { 33 | client = await db.connect(req); 34 | const totalRooms = await roomDB.getCardsByUserId(client, user.userId); 35 | let ongoingRooms = totalRooms.filter((rawRoom) => rawRoom.status === 'ONGOING' && rawRoom.isOut === false); 36 | let completeRooms = totalRooms.filter((rawRoom) => rawRoom.status === 'COMPLETE' && rawRoom.isOut === false); 37 | let failRooms = totalRooms.filter((rawRoom) => rawRoom.status === 'FAIL' || rawRoom.isOut === true); 38 | 39 | const ongoingRoomNum = ongoingRooms.length; 40 | const completeRoomNum = completeRooms.length; 41 | const failRoomNum = failRooms.length; 42 | const totalRoomNum = ongoingRoomNum + completeRoomNum + failRoomNum; 43 | 44 | let rooms = []; 45 | let roomIds = []; 46 | 47 | if (roomType === 'ONGOING') { 48 | roomIds = ongoingRooms.map((room) => room.roomId); 49 | rooms = ongoingRooms; 50 | rooms = _.sortBy(rooms, 'startAt').reverse(); 51 | } else if (roomType === 'COMPLETE') { 52 | roomIds = completeRooms.map((room) => room.roomId); 53 | rooms = completeRooms; 54 | rooms = _.sortBy(rooms, 'outAt').reverse(); 55 | } else if (roomType === 'FAIL') { 56 | roomIds = failRooms.map((room) => room.roomId); 57 | rooms = failRooms; 58 | // 중도 퇴장으로 인한 미완료일 경우, end_date 보정 59 | for (let i = 0; i < rooms.length; i++) { 60 | const room = rooms[i]; 61 | if (room.isOut) { 62 | room.endAt = room.outAt; 63 | } 64 | } 65 | rooms = _.sortBy(rooms, 'endAt').reverse(); 66 | } 67 | 68 | if (lastId !== -1) { 69 | const lastIndex = _.indexOf(roomIds, lastId); 70 | // @error 1. 잘못된 last id 71 | if (lastIndex === -1) { 72 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.INVALID_LASTID)); 73 | } 74 | roomIds = roomIds.slice(lastIndex + 1, lastIndex + 1 + size); 75 | ongoingRooms = ongoingRooms.slice(lastIndex + 1, lastIndex + 1 + size); 76 | } 77 | // 최초 요청시 78 | else { 79 | roomIds = roomIds.slice(0, size); 80 | rooms = rooms.slice(0, size); 81 | } 82 | let roomData = []; 83 | 84 | for (let i = 0; i < rooms.length; i++) { 85 | const room = rooms[i]; 86 | const startDate = dayjs(room.startAt); 87 | const endDate = dayjs(room.endAt); 88 | const now = dayjs().add(9, 'hour'); 89 | const today = dayjs(now.format('YYYY-MM-DD')); 90 | const leftDay = endDate.diff(today, 'day'); 91 | const sparkCount = await sparkDB.countSparkByEntryId(client, room.entryId); 92 | let oneRoom = { 93 | roomId: room.roomId, 94 | roomName: room.roomName, 95 | leftDay, 96 | thumbnail: room.thumbnail, 97 | totalReceivedSpark: parseInt(sparkCount[0].count) ? parseInt(sparkCount[0].count) : 0, 98 | startDate: startDate.format('YYYY-MM-DD'), 99 | endDate: endDate.format('YYYY-MM-DD'), 100 | failDay: null, 101 | comment: room.comment, 102 | }; 103 | if (roomType === 'FAIL') { 104 | const failDay = endDate.diff(startDate, 'day') + 1; 105 | oneRoom.failDay = failDay; 106 | } 107 | roomData.push(oneRoom); 108 | } 109 | 110 | const data = { 111 | nickname: user.nickname, 112 | totalRoomNum, 113 | ongoingRoomNum, 114 | completeRoomNum, 115 | failRoomNum, 116 | rooms: roomData, 117 | }; 118 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_MYROOM_SUCCESS, data)); 119 | } catch (error) { 120 | console.log(error); 121 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 122 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 123 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 124 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 125 | } finally { 126 | client.release(); 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /functions/api/routes/myroom/myroomRoomGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, sparkDB, recordDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const _ = require('lodash'); 9 | 10 | /** 11 | * @인증사진_모아보기 12 | * @route GET /myroom/:roomId?lastId=&size= 13 | * @error 14 | * 1. roomId가 없음 15 | * 2. 존재하지 않는 습관방인 경우 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | let lastId = Number(req.query.lastId); 20 | const size = Number(req.query.size); 21 | const user = req.user; 22 | const { roomId } = req.params; 23 | 24 | let client; 25 | 26 | // @error 1. roomId가 없음 27 | if (!roomId) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 29 | } 30 | 31 | try { 32 | client = await db.connect(req); 33 | 34 | const room = await roomDB.getRoomById(client, roomId); 35 | 36 | // @error 2. 존재하지 않는 습관방인 경우 37 | if (!room) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.NO_CONTENT, responseMessage.GET_ROOM_DATA_FAIL)); 39 | } 40 | 41 | // @error 3. 접근 권한이 없는 유저인 경우 42 | const entry = await roomDB.checkEnteredById(client, roomId, user.userId); 43 | if (!entry) { 44 | return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NOT_MEMBER)); 45 | } 46 | 47 | const pagedRecords = await recordDB.getPagedRecordsByEntryId(client, entry.entryId, lastId, size); 48 | 49 | // 해당하는 record가 없을 경우 50 | if (!pagedRecords.length) { 51 | const data = { 52 | roomName: room.roomName, 53 | records: [], 54 | }; 55 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_MYROOM_DETAIL_SUCCESS, data)); 56 | } 57 | 58 | const recordIds = pagedRecords.map((o) => o.recordId); 59 | 60 | const sparkNums = await sparkDB.countSparkByRecordIds(client, recordIds); 61 | 62 | const records = pagedRecords.map((record) => { 63 | const sparkCount = _.find(sparkNums, { recordId: record.recordId }); 64 | const sparkNum = sparkCount ? Number(sparkCount.sparkNum) : 0; 65 | return { 66 | recordId: record.recordId, 67 | leftDay: 66 - record.day, 68 | certifyingImg: record.certifyingImg, 69 | sparkNum, 70 | status: record.status, 71 | timerRecord: record.timerRecord, 72 | }; 73 | }); 74 | 75 | const data = { 76 | roomName: room.roomName, 77 | records, 78 | }; 79 | 80 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_MYROOM_DETAIL_SUCCESS, data)); 81 | } catch (error) { 82 | console.log(error); 83 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 84 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 85 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 86 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 87 | } finally { 88 | client.release(); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /functions/api/routes/myroom/myroomThumbnailListGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, sparkDB, recordDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const _ = require('lodash'); 9 | 10 | /** 11 | * @대표사진_변경_뷰에서_인증사진_모아보기 12 | * @route GET /myroom/:roomId?lastId=&size= 13 | * @error 14 | * 1. roomId가 없음 15 | * 2. 존재하지 않는 습관방인 경우 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | let lastId = Number(req.query.lastId); 20 | const size = Number(req.query.size); 21 | const user = req.user; 22 | const { roomId } = req.params; 23 | 24 | let client; 25 | 26 | // @error 1. roomId가 없음 27 | if (!roomId) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 29 | } 30 | 31 | try { 32 | client = await db.connect(req); 33 | 34 | const room = await roomDB.getRoomById(client, roomId); 35 | 36 | // @error 2. 존재하지 않는 습관방인 경우 37 | if (!room) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.NO_CONTENT, responseMessage.GET_ROOM_DATA_FAIL)); 39 | } 40 | 41 | // @error 3. 접근 권한이 없는 유저인 경우 42 | const entry = await roomDB.checkEnteredById(client, roomId, user.userId); 43 | if (!entry) { 44 | return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NOT_MEMBER)); 45 | } 46 | 47 | const pagedRecords = await recordDB.getDonePagedRecordsByEntryId(client, entry.entryId, lastId, size); 48 | 49 | // 해당하는 record가 없을 경우 50 | if (!pagedRecords.length) { 51 | const data = { 52 | roomName: room.roomName, 53 | records: [], 54 | }; 55 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_MYROOM_DETAIL_SUCCESS, data)); 56 | } 57 | 58 | const recordIds = pagedRecords.map((o) => o.recordId); 59 | 60 | const sparkNums = await sparkDB.countSparkByRecordIds(client, recordIds); 61 | 62 | const records = pagedRecords.map((record) => { 63 | const sparkCount = _.find(sparkNums, { recordId: record.recordId }); 64 | const sparkNum = sparkCount ? Number(sparkCount.sparkNum) : 0; 65 | return { 66 | recordId: record.recordId, 67 | leftDay: 66 - record.day, 68 | certifyingImg: record.certifyingImg, 69 | sparkNum, 70 | status: record.status, 71 | timerRecord: record.timerRecord, 72 | }; 73 | }); 74 | 75 | const data = { 76 | roomName: room.roomName, 77 | records, 78 | }; 79 | 80 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_THUMBNAIL_LIST_SUCCESS, data)); 81 | } catch (error) { 82 | console.log(error); 83 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 84 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 85 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 86 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 87 | } finally { 88 | client.release(); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /functions/api/routes/myroom/myroomThumbnailPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, sparkDB, recordDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const _ = require('lodash'); 9 | 10 | /** 11 | * @보관함_대표사진_변경 12 | * @route PATCH /myroom/:roomId/thumbnail/:recordId 13 | * @error 14 | * 1. roomId/recordId 가 없음 15 | * 2. 존재하지 않는 습관방인 경우 16 | * 3. 접근 권한이 없는 유저인 경우 17 | * 4. 올바르지 않은 recordId인 경우 (해당 습관방의 recordId가 아님, NONE/REST상태의 record, 없는 record) 18 | */ 19 | 20 | module.exports = async (req, res) => { 21 | const user = req.user; 22 | const { roomId, recordId } = req.params; 23 | 24 | let client; 25 | 26 | // @error 1. roomId/recordId 가 없음 27 | if (!roomId || !recordId) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 29 | } 30 | 31 | try { 32 | client = await db.connect(req); 33 | 34 | const room = await roomDB.getRoomById(client, roomId); 35 | console.log(room); 36 | // @error 2. 존재하지 않는 습관방인 경우 37 | if (!room) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.NO_CONTENT, responseMessage.GET_ROOM_DATA_FAIL)); 39 | } 40 | 41 | // @error 3. 접근 권한이 없는 유저인 경우 42 | const entry = await roomDB.checkEnteredById(client, roomId, user.userId); 43 | if (!entry) { 44 | return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NOT_MEMBER)); 45 | } 46 | 47 | const record = await recordDB.getRecordById(client, recordId); 48 | // @error 4. 올바르지 않은 recordId인 경우 (해당 습관방의 recordId가 아님, NONE/REST상태의 record, 없는 record) 49 | if (!record || record.entryId != entry.entryId || record.status !== 'DONE' || !record.certifyingImg) { 50 | res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.INCORRECT_RECORD)); 51 | } 52 | 53 | await roomDB.updateThumbnail(client, entry.entryId, record.certifyingImg); 54 | 55 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.UPDATE_THUMBNAIL_SUCCESS)); 56 | } catch (error) { 57 | console.log(error); 58 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 59 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 60 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 61 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 62 | } finally { 63 | client.release(); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /functions/api/routes/notice/active/activeGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const dayjs = require('dayjs'); 3 | const util = require('../../../../lib/util'); 4 | const statusCode = require('../../../../constants/statusCode'); 5 | const responseMessage = require('../../../../constants/responseMessage'); 6 | const db = require('../../../../db/db'); 7 | const slackAPI = require('../../../../middlewares/slackAPI'); 8 | const { noticeDB } = require('../../../../db'); 9 | const { passedDayToStr } = require('../../../../lib/passedDayToStr'); 10 | 11 | /** 12 | * @활동_알림_조회 13 | * @route GET /notice/active?lastId=&size= 14 | * @error 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const user = req.user; 19 | const userId = user.userId; 20 | const { lastId, size } = req.query; 21 | 22 | let client; 23 | 24 | try { 25 | client = await db.connect(req); 26 | 27 | const newServiceNum = await noticeDB.getNumberOfUnreadServiceNoticeById(client, userId); 28 | const newService = newServiceNum > 0 ? true : false; 29 | 30 | const actives = await noticeDB.getActivesByUserId(client, userId, parseInt(lastId), parseInt(size)); 31 | let now = dayjs().add(9, 'hour'); 32 | now = now.set('hour', 0); 33 | now = now.set('minute', 0); 34 | now = now.set('second', 1); 35 | 36 | const notices = actives.map((a) => { 37 | const notice = {}; 38 | let createdAt = dayjs(a.createdAt); 39 | createdAt = createdAt.set('hour', 0); 40 | createdAt = createdAt.set('minute', 0); 41 | createdAt = createdAt.set('second', 0); 42 | const passedDay = now.diff(createdAt, 'd'); 43 | 44 | notice['noticeId'] = a.notificationId; 45 | notice['noticeTitle'] = a.title; 46 | notice['noticeContent'] = a.content; 47 | notice['noticeImg'] = a.thumbnail; 48 | notice['isThumbProfile'] = a.isThumbProfile; 49 | notice['day'] = passedDayToStr(passedDay); 50 | notice['isNew'] = !a.isRead; 51 | return notice; 52 | }); 53 | 54 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_ACTIVE_SUCCESS, { newService, notices })); 55 | } catch (error) { 56 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 57 | console.log(error); 58 | 59 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 60 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 61 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 62 | } finally { 63 | client.release(); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /functions/api/routes/notice/active/activeReadPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../../lib/util'); 3 | const statusCode = require('../../../../constants/statusCode'); 4 | const responseMessage = require('../../../../constants/responseMessage'); 5 | const db = require('../../../../db/db'); 6 | const { noticeDB } = require('../../../../db'); 7 | 8 | /** 9 | * @활동_알림_읽음_처리 10 | * @route PATCH /notice/active/read 11 | * @body 12 | * @error 13 | */ 14 | 15 | module.exports = async (req, res) => { 16 | const user = req.user; 17 | const userId = user.userId; 18 | 19 | let client; 20 | 21 | try { 22 | client = await db.connect(req); 23 | await noticeDB.activeReadByUserId(client, userId); 24 | 25 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.ACTIVE_READ_SUCCESS)); 26 | } catch (error) { 27 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 28 | console.log(error); 29 | 30 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 31 | } finally { 32 | client.release(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /functions/api/routes/notice/active/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../../middlewares/auth'); 4 | 5 | router.get('', checkUser, require('./activeGET')); 6 | router.patch('/read', checkUser, require('./activeReadPATCH')); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /functions/api/routes/notice/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../middlewares/auth'); 4 | 5 | router.use('/active', require('./active')); 6 | router.use('/service', require('./service')); 7 | 8 | router.get('/new', checkUser, require('./noticeNewGET')); 9 | router.get('/setting', checkUser, require('./noticeSettingGET')); 10 | router.patch('/setting', checkUser, require('./noticeSettingPATCH')); 11 | router.delete('/:noticeId', checkUser, require('./noticeDELETE')); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /functions/api/routes/notice/noticeDELETE.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { noticeDB } = require('../../../db'); 7 | 8 | /** 9 | * @서비스_및_활동_알림_삭제 10 | * @route GET /notice/:noticeId 11 | * @error 12 | * 1. 알림 id가 전달되지 않음 13 | * 2. 올바르지 않은 noticeId가 전달된 경우 14 | * 3. 알림 삭제 권한이 없는 사용자가 요청을 보낸 경우 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const { noticeId } = req.params; 19 | const user = req.user; 20 | const userId = user.userId; 21 | 22 | // @error 1. 알림 id가 전달되지 않음 23 | if (!noticeId) { 24 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 25 | } 26 | 27 | let client; 28 | 29 | try { 30 | client = await db.connect(req); 31 | 32 | const notice = await noticeDB.getNoticeByNoticeId(client, noticeId); 33 | 34 | // @error 2. 올바르지 않은 noticeId가 전달된 경우 35 | if (!notice) { 36 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOTICE_ID_NOT_VALID)); 37 | } 38 | 39 | // @error 3. 알림 삭제 권한이 없는 사용자가 요청을 보낸 경우 40 | if (userId !== notice.receiverId) { 41 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 42 | } 43 | 44 | await noticeDB.deleteNoticeByNoticeId(client, noticeId); 45 | 46 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.NOTICE_DELETE_SUCCESS)); 47 | } catch (error) { 48 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 49 | console.log(error); 50 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 51 | } finally { 52 | client.release(); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /functions/api/routes/notice/noticeNewGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { noticeDB } = require('../../../db'); 8 | 9 | /** 10 | * @새로운_빨콩_알림_조회 11 | * @route GET /notice/new 12 | * @error 13 | * 14 | */ 15 | 16 | module.exports = async (req, res) => { 17 | const user = req.user; 18 | 19 | let client; 20 | 21 | try { 22 | client = await db.connect(req); 23 | 24 | const numOfUnreadNotice = await noticeDB.getNumberOfUnreadNoticeById(client, user.userId); 25 | 26 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_NEW_NOTICE_SUCCESS, { newNotice: numOfUnreadNotice > 0 ? true : false })); 27 | } catch (error) { 28 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 29 | console.log(error); 30 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 31 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 32 | 33 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 34 | } finally { 35 | client.release(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /functions/api/routes/notice/noticeSettingGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const slackAPI = require('../../../middlewares/slackAPI'); 6 | 7 | /** 8 | * @푸시알림_설정_조회 9 | * @route GET /notice/setting 10 | * @error 11 | * 12 | */ 13 | 14 | module.exports = async (req, res) => { 15 | const user = req.user; 16 | 17 | try { 18 | const { pushRoomStart, pushSpark, pushConsider, pushCertification, pushRemind } = user; 19 | 20 | const data = { 21 | roomStart: pushRoomStart, 22 | spark: pushSpark, 23 | consider: pushConsider, 24 | certification: pushCertification, 25 | remind: pushRemind, 26 | }; 27 | 28 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_NOTICE_SETTING_SUCCESS, data)); 29 | } catch (error) { 30 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 31 | console.log(error); 32 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 33 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 34 | 35 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 36 | } finally { 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /functions/api/routes/notice/noticeSettingPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB } = require('../../../db'); 8 | 9 | /** 10 | * @푸시알림_설정_토글 11 | * @route PATCH /notice/setting 12 | * @error 13 | * 1. 유효하지 않은 category 14 | */ 15 | 16 | module.exports = async (req, res) => { 17 | const user = req.user; 18 | const category = req.query.category; 19 | 20 | // @error 1. 유효하지 않은 category 21 | if (!['roomStart', 'spark', 'consider', 'certification', 'remind'].includes(category)) { 22 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PUSH_CATEGORY_INVALID)); 23 | } 24 | 25 | let client; 26 | 27 | try { 28 | client = await db.connect(req); 29 | 30 | await userDB.togglePushSettingById(client, user.userId, category); 31 | 32 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.PUSH_TOGGLE_SUCCESS)); 33 | } catch (error) { 34 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 35 | console.log(error); 36 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 37 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 38 | 39 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 40 | } finally { 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /functions/api/routes/notice/service/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../../middlewares/auth'); 4 | 5 | router.get('', checkUser, require('./serviceGET')); 6 | router.patch('/read', checkUser, require('./serviceReadPATCH')); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /functions/api/routes/notice/service/serviceGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const dayjs = require('dayjs'); 3 | const util = require('../../../../lib/util'); 4 | const statusCode = require('../../../../constants/statusCode'); 5 | const responseMessage = require('../../../../constants/responseMessage'); 6 | const db = require('../../../../db/db'); 7 | const slackAPI = require('../../../../middlewares/slackAPI'); 8 | const { noticeDB } = require('../../../../db'); 9 | const { passedDayToStr } = require('../../../../lib/passedDayToStr'); 10 | 11 | /** 12 | * @서비스_알림_조회 13 | * @route GET /notice/service?lastId=&size= 14 | * @error 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const user = req.user; 19 | const userId = user.userId; 20 | const { lastId, size } = req.query; 21 | 22 | let client; 23 | 24 | try { 25 | client = await db.connect(req); 26 | 27 | const newActiveNum = await noticeDB.getNumberOfUnreadActiveNoticeById(client, userId); 28 | const newActive = newActiveNum > 0 ? true : false; 29 | 30 | const services = await noticeDB.getServicesByUserId(client, userId, parseInt(lastId), parseInt(size)); 31 | let now = dayjs().add(9, 'hour'); 32 | now = now.set('hour', 0); 33 | now = now.set('minute', 0); 34 | now = now.set('second', 1); 35 | 36 | const notices = services.map((s) => { 37 | const notice = {}; 38 | let createdAt = dayjs(s.createdAt); 39 | createdAt = createdAt.set('hour', 0); 40 | createdAt = createdAt.set('minute', 0); 41 | createdAt = createdAt.set('second', 0); 42 | const passedDay = now.diff(createdAt, 'd'); 43 | 44 | notice['noticeId'] = s.notificationId; 45 | notice['noticeTitle'] = s.title; 46 | notice['noticeContent'] = s.content; 47 | notice['day'] = passedDayToStr(passedDay); 48 | notice['isNew'] = !s.isRead; 49 | return notice; 50 | }); 51 | 52 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_SERVICE_SUCCESS, { newActive, notices })); 53 | } catch (error) { 54 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 55 | console.log(error); 56 | 57 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 58 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 59 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 60 | } finally { 61 | client.release(); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /functions/api/routes/notice/service/serviceReadPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../../lib/util'); 3 | const statusCode = require('../../../../constants/statusCode'); 4 | const responseMessage = require('../../../../constants/responseMessage'); 5 | const db = require('../../../../db/db'); 6 | const { noticeDB } = require('../../../../db'); 7 | 8 | /** 9 | * @서비스_알림_읽음_처리 10 | * @route PATCH /notice/service/read 11 | * @body 12 | * @error 13 | */ 14 | 15 | module.exports = async (req, res) => { 16 | const user = req.user; 17 | const userId = user.userId; 18 | 19 | let client; 20 | 21 | try { 22 | client = await db.connect(req); 23 | await noticeDB.serviceReadByUserId(client, userId); 24 | 25 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SERVICE_READ_SUCCESS)); 26 | } catch (error) { 27 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 28 | console.log(error); 29 | 30 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 31 | } finally { 32 | client.release(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /functions/api/routes/room/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../middlewares/auth'); 4 | const uploadImageIntoSubDir = require('../../../middlewares/uploadImage'); 5 | 6 | router.get('/code/:code', checkUser, require('./roomCodeGET')); 7 | router.post('', checkUser, require('./roomPOST')); 8 | router.post('/:roomId/enter', checkUser, require('./roomEnterPOST')); 9 | router.patch('/:roomId/purpose', checkUser, require('./roomPurposePATCH')); 10 | router.get('/:roomId/waiting', checkUser, require('./roomWaitingGET')); 11 | router.get('/:roomId', checkUser, require('./roomDetailGET')); 12 | router.get('', checkUser, require('./roomListGET')); 13 | router.get('/:roomId/waiting/member', checkUser, require('./roomWaitingMemberGET')); 14 | router.post('/:roomId/start', checkUser, require('./roomStartPOST')); 15 | router.post('/:roomId/status', checkUser, require('./roomStatusPOST')); 16 | router.post('/:roomId/spark', checkUser, require('./sparkPOST')); 17 | router.post('/:roomId/record', checkUser, uploadImageIntoSubDir('certification'), require('./roomRecordPOST')); 18 | router.delete('/:roomId', checkUser, require('./roomDELETE')); 19 | router.delete('/:roomId/out', checkUser, require('./roomOutDELETE')); 20 | router.patch('/:roomId/read', checkUser, require('./roomReadPATCH')); 21 | router.get('/:roomId/timeline', checkUser, require('./roomTimelineGET')); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomCodeGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB, roomDB } = require('../../../db'); 8 | 9 | /** 10 | * @코드로_대기_방_정보_확인 11 | * @route GET /room/code/:code 12 | * @error 13 | * 1. 참여 코드가 전달되지 않음 14 | * 2. 참여코드에 일치하는 방이 없는 경우 15 | * 3. 이미 시작된 습관방인 경우 16 | * 4. 이미 참여중인 방인 경우 17 | * 5. 한번 내보내진 사용자인 경우 18 | * 6. 정원이 가득찬 습관방 19 | */ 20 | 21 | module.exports = async (req, res) => { 22 | const { code } = req.params; 23 | const user = req.user; 24 | const userId = user.userId; 25 | 26 | // @error 1. 참여 코드가 전달되지 않음 27 | if (!code) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 29 | } 30 | 31 | let client; 32 | 33 | try { 34 | client = await db.connect(req); 35 | 36 | const room = await roomDB.getRoomByCode(client, code); 37 | 38 | // @error 2. 참여 코드에 일치하는 방이 없음 39 | if (!room) { 40 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_NULL)); 41 | } 42 | 43 | // @error 3. 참여 코드에 해당하는 방이 대기상태가 아님 44 | if (room.status === 'ONGOING') { 45 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_STARTED)); 46 | } else if (room.status === 'COMPLETE' || room.status === 'FAIL') { 47 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_IMPOSSIBLE)); 48 | } 49 | 50 | // @error 5. 한번 내보내진 사용자인 경우 51 | const kickedHistory = await roomDB.kickedHistoryByIds(client, room.roomId, userId); 52 | if (kickedHistory.length !== 0) { 53 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_IMPOSSIBLE)); 54 | } 55 | 56 | const creator = await userDB.getUserById(client, room.creator); 57 | const entries = await roomDB.getEntriesByRoomId(client, room.roomId); 58 | const imageNum = 3; 59 | let profileImgs = []; 60 | 61 | for (let i = 0; i < entries.length; i++) { 62 | if (i < imageNum) { 63 | let user = await userDB.getUserById(client, entries[i].userId); 64 | profileImgs.push(user.profileImg); 65 | } 66 | 67 | // @error 4. 이미 해당 습관에 참여중인 사용자 68 | if (userId === entries[i].userId) { 69 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_ALREADY)); 70 | } 71 | } 72 | 73 | // @error 6. 정원이 가득찬 습관방 74 | if (entries.length > 9) { 75 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_FULL)); 76 | } 77 | 78 | const data = { 79 | roomId: room.roomId, 80 | roomName: room.roomName, 81 | creatorName: creator.nickname, 82 | creatorImg: creator.profileImg, 83 | profileImgs, 84 | totalNums: entries.length, 85 | }; 86 | 87 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_WAITROOM_DATA_SUCCESS, data)); 88 | } catch (error) { 89 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 90 | console.log(error); 91 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 92 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 93 | 94 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 95 | } finally { 96 | client.release(); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomDELETE.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const { userDB, roomDB, noticeDB } = require('../../../db'); 9 | 10 | /** 11 | * @대기방_삭제하기 12 | * @route DELETE /room/:roomId 13 | * @body 14 | * @error 15 | * 1. roomId가 전달되지 않음 16 | * 2. 존재하지 않는 습관방 17 | * 3. 대기중인 습관방이 아닌 경우 18 | * 4. 요청을 보낸 사용자가 대기방의 호스트가 아닌 경우 19 | */ 20 | 21 | module.exports = async (req, res) => { 22 | const { roomId } = req.params; 23 | const user = req.user; 24 | 25 | // @error 1. roomId가 전달되지 않음 26 | if (!roomId) { 27 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 28 | } 29 | 30 | let client; 31 | 32 | try { 33 | client = await db.connect(req); 34 | 35 | const room = await roomDB.getRoomById(client, roomId); 36 | 37 | // @error 2. 존재하지 않는 습관방 38 | if (!room) { 39 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 40 | } 41 | 42 | // @error 3. 대기중인 습관방이 아닌 경우 43 | if (room.status !== 'NONE') { 44 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_NOT_WAITING)); 45 | } 46 | 47 | // @error 4. 요청을 보낸 사용자가 대기방의 호스트가 아닌 경우 48 | if (room.creator !== user.userId) { 49 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 50 | } 51 | 52 | // 대기방 삭제하기 53 | await roomDB.deleteRoomById(client, room.roomId); 54 | 55 | // 본인을 제외한 참여자들에게 서비스 알림 보내기 56 | const { title, body, isService } = alarmMessage.ROOM_DELETE(room.roomName); 57 | 58 | const friends = await roomDB.getFriendsByIds(client, room.roomId, user.userId); 59 | 60 | const notifications = friends.map((f) => { 61 | return `('${title}', '${body}', '', ${f.userId}, ${isService}, false, ${room.roomId})`; 62 | }); 63 | 64 | if (notifications.length > 0) { 65 | await noticeDB.addNotifications(client, notifications); 66 | } 67 | 68 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.ROOM_DELETE_SUCCESS)); 69 | } catch (error) { 70 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 71 | console.log(error); 72 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 73 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 74 | 75 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 76 | } finally { 77 | client.release(); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomDetailGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, sparkDB, dialogDB, lifeTimelineDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const dayjs = require('dayjs'); 9 | 10 | /** 11 | * @습관방_상세_조회 12 | * @route GET /room/:roomId 13 | * @error 14 | * 1. 존재하지 않는 습관방인 경우 15 | * 2. 진행중인 습관방이 아닌 경우 16 | * 3. 접근 권한이 없는 유저인 경우 17 | */ 18 | 19 | module.exports = async (req, res) => { 20 | const { roomId } = req.params; 21 | const user = req.user; 22 | 23 | let client; 24 | 25 | try { 26 | client = await db.connect(req); 27 | 28 | const room = await roomDB.getRoomById(client, roomId); 29 | 30 | // @error 1. 존재하지 않는 습관방인 경우 31 | if (!room) { 32 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.NO_CONTENT, responseMessage.GET_ROOM_DATA_FAIL)); 33 | } 34 | 35 | const startDate = dayjs(room.startAt); 36 | const endDate = dayjs(room.endAt); 37 | const now = dayjs().add(9, 'hour'); 38 | const today = dayjs(now.format('YYYY-MM-DD')); 39 | const leftDay = endDate.diff(today, 'day'); 40 | const day = dayjs(today).diff(startDate, 'day') + 1; 41 | 42 | // @error 2. 진행중인 습관방이 아닌 경우 43 | if (room.status !== 'ONGOING' || leftDay < 0) { 44 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_ONGOING_ROOM)); 45 | } 46 | const entries = await roomDB.getEntriesByRoomId(client, roomId); 47 | 48 | // @error 3. 접근 권한이 없는 유저인 경우 49 | const userEntry = entries.filter((entry) => entry.userId === user.userId); 50 | if (!userEntry.length) { 51 | return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NOT_MEMBER)); 52 | } 53 | 54 | let isTimelineNew = false; 55 | let newTimelineCount = await lifeTimelineDB.getNewTimelineCount(client, roomId, user.userId); 56 | newTimelineCount > 0 ? (isTimelineNew = true) : (isTimelineNew = false); 57 | 58 | const records = await roomDB.getRecordsByDay(client, roomId, day); 59 | 60 | let myRecord = null; 61 | let considerRecords = []; 62 | let noneRecords = []; 63 | let restRecords = []; 64 | let doneRecords = []; 65 | 66 | records.map((record) => { 67 | if (record.userId === user.userId) { 68 | myRecord = { 69 | recordId: record.recordId, 70 | userId: record.userId, 71 | profileImg: record.profileImg, 72 | nickname: record.nickname, 73 | status: record.status, 74 | rest: record.rest, 75 | }; 76 | } else { 77 | if (record.status === 'CONSIDER') { 78 | considerRecords.push({ 79 | recordId: record.recordId, 80 | userId: record.userId, 81 | profileImg: record.profileImg, 82 | nickname: record.nickname, 83 | status: record.status, 84 | }); 85 | } else if (record.status === 'NONE') { 86 | noneRecords.push({ 87 | recordId: record.recordId, 88 | userId: record.userId, 89 | profileImg: record.profileImg, 90 | nickname: record.nickname, 91 | status: record.status, 92 | }); 93 | } else if (record.status === 'REST') { 94 | restRecords.push({ 95 | recordId: record.recordId, 96 | userId: record.userId, 97 | profileImg: record.profileImg, 98 | nickname: record.nickname, 99 | status: record.status, 100 | }); 101 | } else { 102 | doneRecords.push({ 103 | recordId: record.recordId, 104 | userId: record.userId, 105 | profileImg: record.profileImg, 106 | nickname: record.nickname, 107 | status: record.status, 108 | }); 109 | } 110 | } 111 | }); 112 | 113 | const otherRecords = [...considerRecords, ...noneRecords, ...doneRecords, ...restRecords]; 114 | 115 | const receivedSpark = await sparkDB.countSparkByRecordId(client, myRecord.recordId); 116 | myRecord.receivedSpark = parseInt(receivedSpark.count); 117 | 118 | // new_term false처리 119 | if (userEntry[0].newTerm) { 120 | await roomDB.updateTermFalseByEntryId(client, userEntry[0].entryId); 121 | } 122 | 123 | const data = { 124 | roomId: room.roomId, 125 | roomName: room.roomName, 126 | startDate: startDate.format('YYYY.MM.DD.'), 127 | endDate: endDate.format('YYYY.MM.DD.'), 128 | moment: userEntry[0].moment, 129 | purpose: userEntry[0].purpose, 130 | leftDay, 131 | life: room.life, 132 | fromStart: room.fromStart, 133 | lifeDeductionCount: 0, 134 | isTimelineNew, 135 | isTermNew: userEntry[0].newTerm, 136 | myRecord, 137 | otherRecords, 138 | }; 139 | 140 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_ROOM_DETAIL_SUCCESS, data)); 141 | } catch (error) { 142 | console.log(error); 143 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 144 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 145 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 146 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 147 | } finally { 148 | client.release(); 149 | } 150 | }; 151 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomEnterPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { roomDB } = require('../../../db'); 8 | 9 | /** 10 | * @습관방_참여 11 | * @route POST /room/:roomId/enter 12 | * @body 13 | * @error 14 | * 1. roomId가 올바르지 않음 (이미 삭제된 방이거나 존재하지 않은 방) 15 | * 2. 사용자가 이미 참여중인 방임 16 | * 3. 정원이 가득찬 습관방 17 | * 4. 습관방 참여 실패 18 | */ 19 | 20 | module.exports = async (req, res) => { 21 | const { roomId } = req.params; 22 | const user = req.user; 23 | const userId = user.userId; 24 | 25 | let client; 26 | 27 | try { 28 | client = await db.connect(req); 29 | 30 | const room = await roomDB.getRoomById(client, roomId); 31 | 32 | // @error 1. roomId가 올바르지 않음 (이미 삭제된 방이거나 존재하지 않은 방) 33 | if (!room) { 34 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 35 | } 36 | 37 | // @error 2. 사용자가 이미 참여중인 방임 38 | const isEntered = await roomDB.checkEnteredById(client, roomId, userId); 39 | if (isEntered) { 40 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ENTER_ROOM_ALREADY)); 41 | } 42 | 43 | // error 3. 정원이 가득찬 습관방 44 | const entries = await roomDB.getEntriesByRoomId(client, roomId); 45 | if (entries.length > 9) { 46 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.GET_WAITROOM_DATA_FULL)); 47 | } 48 | 49 | const enterEntry = await roomDB.enterById(client, roomId, userId); 50 | 51 | // @error 4. 습관 방 참여 실패 52 | if (!enterEntry) { 53 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ENTER_ROOM_FAIL)); 54 | } 55 | 56 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.ENTER_ROOM_SUCCESS)); 57 | } catch (error) { 58 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 59 | console.log(error); 60 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 61 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 62 | 63 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 64 | } finally { 65 | client.release(); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomListGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, dialogDB } = require('../../../db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const dayjs = require('dayjs'); 9 | const _ = require('lodash'); 10 | 11 | /** 12 | * @습관방_리스트_조회 13 | * @route GET /room?lastId=&size= 14 | * @error 15 | * 1. lastId 또는 size 값이 전달되지 않음 16 | * 2. 잘못된 lastId 17 | */ 18 | 19 | module.exports = async (req, res) => { 20 | const lastId = Number(req.query.lastId); 21 | const size = Number(req.query.size); 22 | const user = req.user; 23 | 24 | // @error 1. lastId 또는 size 값이 전달되지 않음 25 | if (!lastId || !size) { 26 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 27 | } 28 | 29 | let client; 30 | 31 | try { 32 | client = await db.connect(req); 33 | let dialogs = await dialogDB.getUserDialogs(client, user.userId, ["'COMPLETE'", "'FAIL'"]); 34 | dialogs = _.sortBy(dialogs, 'type'); 35 | const rawRooms = await roomDB.getRoomsByUserId(client, user.userId); 36 | let waitingRooms = rawRooms.filter((rawRoom) => rawRoom.status === 'NONE'); 37 | waitingRooms = _.sortBy(waitingRooms, 'createdAt').reverse(); // 최근에 생성된 대기방이 위로 38 | let ongoingRooms = rawRooms.filter((rawRoom) => rawRoom.status === 'ONGOING'); 39 | ongoingRooms = _.sortBy(ongoingRooms, 'startTime').reverse(); // 최근에 시작한 습관방이 위로 40 | 41 | const dialogRoomIds = [...new Set(dialogs.filter(Boolean).map((room) => room.roomId))]; 42 | const waitingRoomIds = [...new Set(waitingRooms.filter(Boolean).map((room) => room.roomId))]; 43 | const ongoingRoomIds = [...new Set(ongoingRooms.filter(Boolean).map((room) => room.roomId))]; 44 | const roomIds = waitingRoomIds.concat(dialogRoomIds.concat(ongoingRoomIds)); 45 | 46 | let responseRoomIds = []; 47 | 48 | // 최초 요청이 아닐시 49 | if (lastId !== -1) { 50 | const lastIndex = _.indexOf(roomIds, lastId); 51 | // @error 2. 잘못된 last Id 52 | if (lastIndex === -1) { 53 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.INVALID_LASTID)); 54 | } 55 | responseRoomIds = roomIds.slice(lastIndex + 1, lastIndex + 1 + size); 56 | } 57 | // 최초 요청시 58 | else { 59 | responseRoomIds = roomIds.slice(0, size); 60 | } 61 | 62 | // 마지막일 때 -> 빈 배열 return 63 | if (!responseRoomIds.length) { 64 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_ROOM_LIST_SUCCESS, { rooms: [] })); 65 | } 66 | 67 | const rawRoomInfo = await roomDB.getRoomsByIds(client, responseRoomIds); 68 | const roomInfoMap = new Map(); 69 | rawRoomInfo.map((o) => { 70 | roomInfoMap.set(o.roomId, o); 71 | }); 72 | let rooms = []; 73 | 74 | const roomInfo = rawRoomInfo.sort((a, b) => responseRoomIds.indexOf(a.roomId) - responseRoomIds.indexOf(b.roomId)); 75 | const today = dayjs(dayjs().add(9, 'hour').format('YYYY-M-D')); 76 | 77 | // roomIds 빈 배열일 때 처리 78 | const rawProfiles = await roomDB.getUserProfilesByRoomIds(client, responseRoomIds, today); 79 | const profiles = rawProfiles.sort((a, b) => responseRoomIds.indexOf(a.roomId) - responseRoomIds.indexOf(b.roomId)); 80 | 81 | let dialogRecords = []; 82 | if (dialogRoomIds.length > 0) { 83 | dialogRecords = await roomDB.getAllRecordsByUserIdAndRoomIds(client, user.userId, dialogRoomIds); 84 | } 85 | 86 | rooms = responseRoomIds.map((roomId) => { 87 | const isDialog = dialogRoomIds.includes(roomId); 88 | const roomInfo = roomInfoMap.get(roomId); 89 | let dialog; 90 | let status = 'NONE'; 91 | let doneMemberNum = 0; 92 | let isUploaded = false; 93 | if (isDialog) { 94 | dialog = dialogs.filter((o) => o.roomId === roomId)[0]; 95 | // dialog이면 status로 dialogType (COMPLETE / FAIL) 전달 96 | status = dialog.type; 97 | const endRecord = _(dialogRecords) 98 | .filter((o) => o.roomId === roomId) 99 | .sortBy('date') 100 | .value() 101 | .reverse()[0]; 102 | if (endRecord.status === 'DONE') { 103 | isUploaded = true; 104 | } 105 | } else { 106 | const userStatus = profiles.filter(Boolean).filter((o) => { 107 | if (o.roomId === roomId && o.userId === user.userId) { 108 | return true; 109 | } 110 | return false; 111 | }); 112 | 113 | // myStatus가 CONSIDER인 경우, NONE으로 전달 114 | const myStatus = userStatus[0].status; 115 | if (myStatus !== 'CONSIDER') { 116 | status = myStatus; 117 | } 118 | 119 | if (myStatus === 'DONE') { 120 | isUploaded = true; 121 | } 122 | } 123 | 124 | const doneMembers = profiles.filter(Boolean).filter((o) => { 125 | if (o.roomId === roomId && (o.status === 'REST' || o.status === 'DONE')) { 126 | return true; 127 | } 128 | return false; 129 | }); 130 | doneMemberNum = doneMembers.length; 131 | 132 | let profileImgs = profiles 133 | .filter(Boolean) 134 | .filter((o) => o.roomId === roomId) 135 | .map((o) => o.profileImg); 136 | 137 | const memberNum = profileImgs.length; 138 | if (profileImgs.length < 3) { 139 | const length = profileImgs.length; 140 | 141 | for (let i = 0; i < 3 - length; i++) { 142 | profileImgs.push(null); 143 | } 144 | } else { 145 | profileImgs = profileImgs.slice(0, 3); 146 | } 147 | 148 | const endDate = dayjs(roomInfo.endAt); 149 | let leftDay = 0; 150 | if (isDialog) { 151 | if (dialog.type === 'COMPLETE') { 152 | leftDay = 0; 153 | } else { 154 | const startDate = dayjs(roomInfo.startAt); 155 | // 기존: startDate + 65 = endDate 156 | // startDate + 65 - endDate = leftDay 157 | // leftDay = 65 - (endDate - startDate) 158 | leftDay = 65 - endDate.diff(startDate, 'day'); 159 | } 160 | } else { 161 | leftDay = endDate.diff(today, 'day'); 162 | } 163 | 164 | const room = { 165 | roomId, 166 | roomName: roomInfo.roomName, 167 | leftDay, 168 | profileImg: profileImgs, 169 | life: roomInfo.life, 170 | isStarted: roomInfo.status === 'NONE' ? false : true, 171 | myStatus: status, 172 | memberNum, 173 | doneMemberNum, 174 | isUploaded, 175 | }; 176 | 177 | return room; 178 | }); 179 | 180 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_ROOM_LIST_SUCCESS, { rooms })); 181 | } catch (error) { 182 | console.log(error); 183 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 184 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 185 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 186 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 187 | } finally { 188 | client.release(); 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomOutDELETE.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const slackAPI = require('../../../middlewares/slackAPI'); 8 | const { userDB, roomDB, noticeDB } = require('../../../db'); 9 | 10 | /** 11 | * @대기방_및_습관방_나가기 12 | * @route DELETE /room/:roomId/out 13 | * @body 14 | * @error 15 | * 1. roomId가 전달되지 않음 16 | * 2. 존재하지 않는 습관방 17 | * 3. 유저가 해당 습관방에 참여하지 않는 경우 18 | * 4. 본인이 host인데 대기방 나가기 요청을 보낸 경우 19 | */ 20 | 21 | module.exports = async (req, res) => { 22 | const { roomId } = req.params; 23 | const user = req.user; 24 | const userId = user.userId; 25 | 26 | // @error 1. roomId가 전달되지 않음 27 | if (!roomId) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 29 | } 30 | 31 | let client; 32 | 33 | try { 34 | client = await db.connect(req); 35 | 36 | const room = await roomDB.getRoomById(client, roomId); 37 | // @error 2. 존재하지 않는 습관방 38 | if (!room) { 39 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 40 | } 41 | 42 | const entry = await roomDB.getEntryByIds(client, roomId, user.userId); 43 | 44 | // @error 3. 유저가 해당 습관방에 참여하지 않는 경우 45 | if (!entry) { 46 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_MEMBER)); 47 | } 48 | 49 | // @error 4. 본인이 host인데 대기방 나가기 요청을 보낸 경우 50 | if (room.status === 'NONE' && userId === room.creator) { 51 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.HOST_WAITROOM_OUT_FAIL)); 52 | } 53 | 54 | // 대기방 또는 습관방 나가기 55 | await roomDB.outById(client, room, userId); 56 | const friends = await roomDB.getFriendsByIds(client, roomId, userId); 57 | 58 | // 습관방의 마지막 인원이었다면, 방 END 처리 59 | if (!friends.length) { 60 | await roomDB.endById(client, roomId); 61 | } 62 | 63 | // 진행중인 방을 나갔을 경우에는 본인을 제외한 참여자들에게 활동 알림 보내기 64 | if (room.status === 'ONGOING') { 65 | const { title, body, isService } = alarmMessage.ROOM_OUT(user.nickname, room.roomName); 66 | 67 | for (let i = 0; i < friends.length; i++) { 68 | const target = await userDB.getUserById(client, friends[i].userId); 69 | await noticeDB.addNotification(client, title, body, user.profileImg, target.userId, isService, true, room.roomId); 70 | } 71 | } 72 | 73 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.ROOM_OUT_SUCCESS)); 74 | } catch (error) { 75 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 76 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 77 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 78 | 79 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 80 | } finally { 81 | client.release(); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { roomDB } = require('../../../db'); 8 | const { nanoid } = require('nanoid'); 9 | 10 | /** 11 | * @습관방_생성 12 | * @route POST /room 13 | * @body roomName:string, fromStart:boolean 14 | * @error 15 | * 1. 습관방 이름 / 습관방 타입이 전달되지 않음 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | const { roomName, fromStart } = req.body; 20 | const user = req.user; 21 | const userId = user.userId; 22 | 23 | console.log(roomName, fromStart); 24 | 25 | // error 1. 습관방 이름 또는 타입이 전달되지 않음 26 | if (!roomName || typeof fromStart !== 'boolean') { 27 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 28 | } 29 | 30 | let client, code; 31 | let isCodeUnique = false; 32 | 33 | try { 34 | client = await db.connect(req); 35 | while (!isCodeUnique) { 36 | code = nanoid(7); 37 | isCodeUnique = await roomDB.isCodeUnique(client, code); 38 | } 39 | 40 | // 습관 방 생성 41 | const room = await roomDB.addRoom(client, roomName, code, userId, fromStart); 42 | const roomId = room.roomId; 43 | 44 | // 생성한 방에 입장 45 | await roomDB.enterById(client, roomId, userId); 46 | 47 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.CREATE_ROOM_SUCCESS, { roomId })); 48 | } catch (error) { 49 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 50 | console.log(error); 51 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 52 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 53 | 54 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 55 | } finally { 56 | client.release(); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomPurposePATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { roomDB } = require('../../../db'); 8 | 9 | /** 10 | * @나의_목표_설정하기 11 | * @route PATCH /room/:roomId/purpose 12 | * @body moment:string, purpose:boolean 13 | * @error 14 | * 1. moment 또는 purpose가 전달되지 않음 15 | * 2. 권한이 없는 사용자로부터의 요청 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | const { moment, purpose } = req.body; 20 | const { roomId } = req.params; 21 | const user = req.user; 22 | const userId = user.userId; 23 | 24 | // error 1. moment 또는 purpose가 전달되지 않음 25 | if (!moment || !purpose) { 26 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 27 | } 28 | 29 | let client; 30 | 31 | try { 32 | client = await db.connect(req); 33 | 34 | let entry = await roomDB.getEntryByIds(client, roomId, userId); 35 | 36 | // error 2. 권한이 없는 사용자로부터의 요청 37 | if (!entry) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 39 | } 40 | 41 | const entryId = entry.entryId; 42 | entry = await roomDB.updatePurposeByEntryId(client, entryId, moment, purpose); 43 | 44 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.PURPOSE_SET_SUCCESS)); 45 | } catch (error) { 46 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 47 | console.log(error); 48 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 49 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 50 | 51 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 52 | } finally { 53 | client.release(); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomReadPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { roomDB, dialogDB } = require('../../../db'); 8 | 9 | /** 10 | * @나의_목표_설정하기 11 | * @route PATCH /room/:roomId/read 12 | * @error 13 | * 1. 잘못된 roomId 14 | * 2. 권한이 없는 사용자로부터의 요청 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const { roomId } = req.params; 19 | const user = req.user; 20 | const userId = user.userId; 21 | let client; 22 | 23 | try { 24 | client = await db.connect(req); 25 | 26 | const dialog = await dialogDB.getUnReadDialogByRoomAndUser(client, roomId, userId); 27 | if (!dialog) { 28 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 29 | } 30 | let entry = await roomDB.getEntryByIds(client, roomId, userId); 31 | 32 | // error 2. 권한이 없는 사용자로부터의 요청 33 | if (!entry) { 34 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 35 | } 36 | 37 | await dialogDB.setDialogRead(client, dialog.dialogId); 38 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.DIALOG_READ_SUCCESS)); 39 | } catch (error) { 40 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 41 | console.log(error); 42 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 43 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 44 | 45 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 46 | } finally { 47 | client.release(); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomRecordPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const pushAlarm = require('../../../lib/pushAlarm'); 8 | const slackAPI = require('../../../middlewares/slackAPI'); 9 | const { roomDB, recordDB, noticeDB } = require('../../../db'); 10 | 11 | /** 12 | * @습관인증하기 13 | * @route POST /room/:roomId/record 14 | * @body img: file, timerRecord: string 15 | * @error 16 | * 1. certiftyingImg가 전달되지 않음 17 | * 2. 해당 roomId의 습관방이 존재하지 않음 18 | * 3. 현재 진행중인 습관방이 아님 19 | * 4. 해당 습관방의 member가 아님 20 | * 5. 이미 인증 완료하거나 쉴래요 한 member 21 | */ 22 | 23 | module.exports = async (req, res) => { 24 | const { roomId } = req.params; 25 | const user = req.user; 26 | const certifyingImg = req.imageUrls; 27 | let { timerRecord } = req.body; 28 | if (!timerRecord) { 29 | timerRecord = null; 30 | } 31 | // @error 1. certifyingImg가 전달되지 않음 32 | if (!certifyingImg) { 33 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 34 | } 35 | 36 | let client; 37 | 38 | try { 39 | client = await db.connect(req); 40 | 41 | const room = await roomDB.getRoomById(client, roomId); 42 | // @error 2. 해당 roomId의 습관방이 존재하지 않음 43 | if (!room) { 44 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 45 | } 46 | const entry = await roomDB.getEntryByIds(client, roomId, user.userId); 47 | 48 | // @error 3. 현재 진행중인 습관방이 아님 49 | if (room.status !== 'ONGOING') { 50 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_ONGOING_ROOM)); 51 | } 52 | 53 | // @error 4. 해당 습관방의 멤버가 아님 54 | if (!entry) { 55 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_MEMBER)); 56 | } 57 | 58 | const record = await recordDB.getRecentRecordByEntryId(client, entry.entryId); 59 | 60 | // @error 5. 이미 인증 완료하거나 쉴래요 한 MEMBER 61 | if (record.status === 'DONE' || record.status === 'REST') { 62 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.DONE_OR_REST_MEMBER)); 63 | } 64 | 65 | // 인증한 record update 66 | await recordDB.uploadRecord(client, record.recordId, certifyingImg[0], timerRecord); 67 | 68 | if (!entry.thumbnail) { 69 | await roomDB.updateThumbnail(client, entry.entryId, certifyingImg[0]); 70 | // @TODO: 첫번째 습관 인증 축하 알림 71 | } 72 | 73 | // 인증을 완료하면 본인을 제외한 참여자들에게 알림 및 푸시알림 보내기 74 | const friends = await roomDB.getFriendsByIds(client, roomId, user.userId); 75 | const receiverTokens = friends.filter((f) => f.pushCertification).map((f) => f.deviceToken); 76 | const { title, body, isService, category } = alarmMessage.CERTIFICATION_COMPLETE(user.nickname, room.roomName); 77 | 78 | const notifications = friends.map((f) => { 79 | return `('${title}', '${body}', '${certifyingImg[0]}', ${f.userId}, ${isService}, false, ${room.roomId})`; 80 | }); 81 | 82 | if (notifications.length) { 83 | await noticeDB.addNotifications(client, notifications); 84 | } 85 | 86 | if (receiverTokens.length) { 87 | pushAlarm.sendMulticastByTokens(req, res, title, body, receiverTokens, category, certifyingImg[0], roomId, record.recordId); 88 | } 89 | 90 | const data = { 91 | userId: user.userId, 92 | nickname: user.nickname, 93 | profileImg: user.profileImg, 94 | roomId: room.roomId, 95 | roomName: room.roomName, 96 | recordId: record.recordId, 97 | day: record.day, 98 | certifyingImg: record.certifyingImg, 99 | timerRecord: record.timerRecord, 100 | }; 101 | 102 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.CERTIFY_SUCCESS, data)); 103 | } catch (error) { 104 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 105 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 106 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 107 | 108 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 109 | } finally { 110 | client.release(); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomStartPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const pushAlarm = require('../../../lib/pushAlarm'); 8 | const dayjs = require('dayjs'); 9 | const slackAPI = require('../../../middlewares/slackAPI'); 10 | const { roomDB, recordDB, noticeDB, lifeTimelineDB } = require('../../../db'); 11 | 12 | /** 13 | * @습관방_시작 14 | * @route POST /room/:roomId/start 15 | * @body 16 | * @error 17 | * 1. roomId가 숫자가 아님 18 | * 2. 유효하지 않은 roomId 19 | * 3. 권한이 없는 사용자로부터의 요청 20 | * 4. 이미 시작된 방 21 | */ 22 | 23 | module.exports = async (req, res) => { 24 | const { roomId } = req.params; 25 | const user = req.user; 26 | const userId = user.userId; 27 | 28 | let client; 29 | 30 | try { 31 | client = await db.connect(req); 32 | 33 | // @error 1. roomId가 숫자가 아님 34 | if (isNaN(roomId)) { 35 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_NOT_FOUND)); 36 | } 37 | 38 | let room = await roomDB.getRoomById(client, roomId); 39 | 40 | // @error 2. 유효하지 않은 roomId 41 | if (!room) { 42 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_NOT_FOUND)); 43 | } 44 | 45 | // @error 3. 권한이 없는 사용자로부터의 요청 46 | if (userId !== room.creator) { 47 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 48 | } 49 | 50 | // 습관방 status ONGOING으로 변경 51 | room = await roomDB.startRoomById(client, roomId); 52 | 53 | // @error 4. 이미 시작된 방 54 | if (!room) { 55 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.START_ROOM_ALREADY)); 56 | } 57 | 58 | const entries = await roomDB.getEntriesByRoomIds(client, [roomId]); 59 | 60 | let insertRecords = []; 61 | let insertTimelines = []; 62 | for (let i = 0; i < entries.length; i++) { 63 | const entry = entries[i]; 64 | // 추가해줄 record들의 속성들 빚어주기 65 | const startDate = dayjs(entry.startAt); 66 | const now = dayjs().add(9, 'hour'); 67 | const today = now.format('YYYY-MM-DD'); 68 | const day = dayjs(today).diff(startDate, 'day') + 1; 69 | const record = '(' + entry.entryId + ",'" + now.format('YYYY-MM-DD') + "'," + day + ')'; 70 | 71 | insertRecords.push(record); 72 | insertTimelines.push(`('${entry.userId}', '${entry.roomId}', false, 1)`); 73 | } 74 | 75 | if (insertRecords.length > 0) { 76 | // 참여자들의 1일차 record 생성 77 | await recordDB.insertRecords(client, insertRecords); 78 | } 79 | 80 | if (insertTimelines.length > 0) { 81 | // 참여자들의 1일차 Life Timeline 생성 82 | await lifeTimelineDB.addFillTimelines(client, insertTimelines); 83 | } 84 | 85 | const { title, body, isService, category } = alarmMessage.ROOM_NEW(room.roomName); 86 | 87 | const allUsers = await roomDB.getAllUsersById(client, roomId); 88 | 89 | // 푸시알림 전송 90 | const receiverTokens = allUsers.filter((u) => u.pushRoomStart).map((u) => u.deviceToken); 91 | pushAlarm.sendMulticastByTokens(req, res, title, body, receiverTokens, category, null, roomId); 92 | 93 | // notification 생성 94 | const notifications = allUsers.map((u) => { 95 | return `('${title}', '${body}', '', ${u.userId}, ${isService}, false, ${room.roomId})`; 96 | }); 97 | 98 | if (notifications.length) { 99 | await noticeDB.addNotifications(client, notifications); 100 | } 101 | 102 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.START_ROOM_SUCCESS)); 103 | } catch (error) { 104 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 105 | console.log(error); 106 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 107 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 108 | 109 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 110 | } finally { 111 | client.release(); 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomStatusPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const pushAlarm = require('../../../lib/pushAlarm'); 8 | const slackAPI = require('../../../middlewares/slackAPI'); 9 | const { userDB, roomDB, recordDB, noticeDB } = require('../../../db'); 10 | 11 | /** 12 | * @쉴래요_고민중 13 | * @route POST /room/:roomId/status 14 | * @body 15 | * @error 16 | * 1. 유효하지 않은 statusType 17 | * 2. 유효하지 않은 roomId 18 | * 3. 권한이 없는 사용자로부터의 요청 19 | * 4. 습관 시작하지 않은 방에서의 요청 20 | * 5. 이미 인증을 완료한 사용자로부터의 요청 21 | * 6. 이미 쉴래요를 사용한 사용자로부터의 요청 22 | * 7. 쉴래요 잔여횟수가 0인 사용쟈로부터의 요청 23 | * 8. 이미 고민중 상태의 사용자로부터의 요청 24 | */ 25 | 26 | module.exports = async (req, res) => { 27 | const { statusType } = req.body; 28 | const { roomId } = req.params; 29 | const user = req.user; 30 | const userId = user.userId; 31 | 32 | let client; 33 | 34 | try { 35 | client = await db.connect(req); 36 | 37 | const room = await roomDB.getRoomById(client, roomId); 38 | 39 | // @error 1. 유효하지 않은 statusType 40 | if (statusType !== 'REST' && statusType !== 'CONSIDER') { 41 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.INVALID_USER_STATUS)); 42 | } 43 | 44 | // @error 2. 유효하지 않은 roomId 45 | if (!room) { 46 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_NOT_FOUND)); 47 | } 48 | 49 | const entry = await roomDB.getEntryByIds(client, roomId, userId); 50 | 51 | // @error 3. 권한이 없는 사용자로부터의 요청 52 | if (!entry) { 53 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 54 | } 55 | 56 | const recentRecord = await recordDB.getRecentRecordByEntryId(client, entry.entryId); 57 | 58 | // @error 4. 습관 시작하지 않은 방에서의 요청 59 | if (!recentRecord) { 60 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_STARTED_ROOM)); 61 | } 62 | 63 | // @error 5. 이미 인증을 완료한 사용자로부터의 요청 64 | if (recentRecord.status === 'DONE') { 65 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.CERTIFICATION_ALREADY_DONE)); 66 | } 67 | 68 | // @error 6. 이미 인증을 완료한 사용자로부터의 요청 69 | if (recentRecord.status === 'REST') { 70 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.REST_ALREADY_DONE)); 71 | } 72 | 73 | if (statusType === 'REST') { 74 | // @error 7. 쉴래요 잔여횟수가 0인 사용쟈로부터의 요청 75 | const rawRest = await roomDB.getRestCountByIds(client, roomId, userId); 76 | const restCount = rawRest.rest; 77 | if (restCount < 1) { 78 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.REST_COUNT_ZERO)); 79 | } 80 | 81 | const newRestCount = restCount - 1; 82 | await roomDB.updateRestByIds(client, roomId, userId, newRestCount); 83 | } 84 | 85 | await recordDB.updateStatusByRecordId(client, recentRecord.recordId, statusType); 86 | 87 | // 고민중을 눌렀으면 Notification에 추가, PushAlarm 전송 88 | if (statusType === 'CONSIDER') { 89 | // @error 8. 이미 고민중 상태의 사용자로부터의 요청 90 | if (recentRecord.status === 'CONSIDER') { 91 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.CONSIDER_ALREADY_DONE)); 92 | } 93 | const sender = await userDB.getUserById(client, userId); 94 | const friends = await roomDB.getFriendsByIds(client, roomId, userId); 95 | const receiverTokens = friends.filter((f) => f.pushConsider).map((f) => f.deviceToken); 96 | const { title, body, isService, category } = alarmMessage.STATUS_CONSIDERING(sender.nickname, room.roomName); 97 | 98 | const notifications = friends.map((f) => { 99 | return `('${title}', '${body}', '${user.profileImg}', ${f.userId}, ${isService}, true, ${room.roomId})`; 100 | }); 101 | 102 | if (notifications.length) { 103 | await noticeDB.addNotifications(client, notifications); 104 | } 105 | 106 | if (receiverTokens.length > 0) { 107 | pushAlarm.sendMulticastByTokens(req, res, title, body, receiverTokens, category, null, roomId); 108 | } 109 | } 110 | 111 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.UPDATE_STATUS_SUCCESS)); 112 | } catch (error) { 113 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 114 | console.log(error); 115 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 116 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 117 | 118 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 119 | } finally { 120 | client.release(); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomTimelineGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { roomDB, lifeTimelineDB } = require('../../../db'); 7 | const timelineMessage = require('../../../constants/lifeTimelineMessage'); 8 | const slackAPI = require('../../../middlewares/slackAPI'); 9 | const dayjs = require('dayjs'); 10 | const { passedDayToStr } = require('../../../lib/passedDayToStr'); 11 | 12 | /** 13 | * @습관방_생명_타임라인_조회 14 | * @route GET /room/:roomId/timeline 15 | * @error 16 | * 1. 존재하지 않는 습관방인 경우 17 | * 2. 진행중인 습관방이 아닌 경우 18 | * 3. 접근 권한이 없는 유저인 경우 19 | */ 20 | 21 | module.exports = async (req, res) => { 22 | const { roomId } = req.params; 23 | const user = req.user; 24 | 25 | let client; 26 | 27 | try { 28 | client = await db.connect(req); 29 | 30 | const room = await roomDB.getRoomById(client, roomId); 31 | 32 | // @error 1. 존재하지 않는 습관방인 경우 33 | if (!room) { 34 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.NO_CONTENT, responseMessage.GET_ROOM_DATA_FAIL)); 35 | } 36 | 37 | const endDate = dayjs(room.endAt); 38 | const now = dayjs().add(9, 'hour'); 39 | const today = dayjs(now.format('YYYY-MM-DD')); 40 | const leftDay = endDate.diff(today, 'day'); 41 | 42 | // @error 2. 진행중인 습관방이 아닌 경우 43 | if (room.status !== 'ONGOING' || leftDay < 0) { 44 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_ONGOING_ROOM)); 45 | } 46 | 47 | const entries = await roomDB.getEntriesByRoomId(client, roomId); 48 | const userEntry = entries.filter((entry) => entry.userId === user.userId); 49 | // @error 3. 접근 권한이 없는 유저인 경우 50 | if (!userEntry.length) { 51 | return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NOT_MEMBER)); 52 | } 53 | 54 | let timelines = await lifeTimelineDB.getLifeTimeline(client, roomId, user.userId); 55 | 56 | timelines = timelines.map((t) => { 57 | if (t.isDecrease) { 58 | const timeline = {}; 59 | const { title, content } = timelineMessage.LIFE_DECREASE(t.decreaseCount); 60 | 61 | let createdAt = dayjs(t.createdAt); 62 | const passedDay = now.diff(createdAt, 'd'); 63 | const profiles = [t.profile_1, t.profile_2].filter((profile) => profile !== 'null'); 64 | 65 | timeline['title'] = title; 66 | timeline['content'] = content; 67 | timeline['profiles'] = profiles; 68 | timeline['day'] = passedDayToStr(passedDay); 69 | timeline['isNew'] = !t.isRead; 70 | 71 | return timeline; 72 | } else { 73 | const timeline = {}; 74 | const { title, content } = timelineMessage.LIFE_FILL(t.termDay); 75 | 76 | let createdAt = dayjs(t.createdAt); 77 | const passedDay = now.diff(createdAt, 'd'); 78 | 79 | timeline['title'] = title; 80 | timeline['content'] = content; 81 | timeline['day'] = passedDayToStr(passedDay); 82 | timeline['isNew'] = !t.isRead; 83 | 84 | return timeline; 85 | } 86 | }); 87 | 88 | await lifeTimelineDB.readLifeTimeline(client, roomId, user.userId); 89 | 90 | return res.status(statusCode.OK).send( 91 | util.success(statusCode.OK, responseMessage.GET_LIFE_TIMELINE_SUCCESS, { 92 | timelines, 93 | }), 94 | ); 95 | } catch (error) { 96 | console.log(error); 97 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 98 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 99 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 100 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 101 | } finally { 102 | client.release(); 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomWaitingGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB, roomDB } = require('../../../db'); 8 | 9 | /** 10 | * @대기_방_조회 11 | * @route GET /room/:roomId/waiting 12 | * @error 13 | * 1. 유효하지 않은 roomId 14 | * 2. 권한이 없는 사용자로부터의 요청 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const { roomId } = req.params; 19 | const user = req.user; 20 | console.log(user); 21 | const userId = user.userId; 22 | 23 | let client; 24 | 25 | try { 26 | client = await db.connect(req); 27 | 28 | const room = await roomDB.getRoomById(client, roomId); 29 | 30 | // @error 1. 유효하지 않은 roomId 31 | if (!room) { 32 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_NOT_FOUND)); 33 | } 34 | 35 | const selfEntry = await roomDB.getEntryByIds(client, roomId, userId); 36 | 37 | // error 2. 권한이 없는 사용자로부터의 요청 38 | if (!selfEntry) { 39 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 40 | } 41 | 42 | // 요청을 보낸 사용자 본인 data 43 | const reqUser = { 44 | userId, 45 | nickname: user.nickname, 46 | profileImg: user.profileImg, 47 | isPurposeSet: selfEntry.moment !== null && selfEntry.purpose !== null, 48 | moment: selfEntry.moment, 49 | purpose: selfEntry.purpose, 50 | isHost: room.creator === userId, 51 | }; 52 | 53 | // 요청을 보낸 사용자를 제외한 member list 54 | const friendsEntries = await roomDB.getFriendsByIds(client, roomId, userId); 55 | const friendsIds = friendsEntries.map((f) => f.userId); 56 | 57 | let members = []; 58 | 59 | // 대기방에 참여중인 친구가 없는 경우 60 | if (friendsIds.length !== 0) { 61 | const users = await userDB.getUsersByIds(client, friendsIds); 62 | users.map((u) => 63 | members.push({ 64 | userId: u.userId, 65 | nickname: u.nickname, 66 | profileImg: u.profileImg, 67 | }), 68 | ); 69 | } 70 | 71 | const data = { 72 | roomId: parseInt(roomId), 73 | roomName: room.roomName, 74 | roomCode: room.code, 75 | fromStart: room.fromStart, 76 | reqUser, 77 | members, 78 | }; 79 | 80 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_WAITROOM_DATA_SUCCESS, data)); 81 | } catch (error) { 82 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 83 | console.log(error); 84 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 85 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 86 | 87 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 88 | } finally { 89 | client.release(); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /functions/api/routes/room/roomWaitingMemberGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB, roomDB } = require('../../../db'); 8 | 9 | /** 10 | * @대기_방_인원_조회 11 | * @route GET /room/:roomId/waiting/member 12 | * @error 13 | * 1. 유효하지 않은 roomId 14 | * 2. 권한이 없는 사용자로부터의 요청 15 | */ 16 | 17 | module.exports = async (req, res) => { 18 | const { roomId } = req.params; 19 | const user = req.user; 20 | const userId = user.userId; 21 | 22 | let client; 23 | 24 | try { 25 | client = await db.connect(req); 26 | 27 | const room = await roomDB.getRoomById(client, roomId); 28 | 29 | // @error 1. 유효하지 않은 roomId 30 | if (!room) { 31 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_NOT_FOUND)); 32 | } 33 | 34 | const selfEntry = await roomDB.getEntryByIds(client, roomId, userId); 35 | 36 | // error 2. 권한이 없는 사용자로부터의 요청 37 | if (!selfEntry) { 38 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.PRIV_NOT_FOUND)); 39 | } 40 | 41 | // 요청을 보낸 사용자를 제외한 member list 42 | const friendsEntries = await roomDB.getFriendsByIds(client, roomId, userId); 43 | if (!friendsEntries.length) { 44 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_WAITROOM_DATA_SUCCESS, { members: [] })); 45 | } 46 | const friendsIds = friendsEntries.map((f) => f.userId); 47 | const users = await userDB.getUsersByIds(client, friendsIds); 48 | let members = []; 49 | 50 | users.map((u) => 51 | members.push({ 52 | userId: u.userId, 53 | nickname: u.nickname, 54 | profileImg: u.profileImg, 55 | }), 56 | ); 57 | 58 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_WAITROOM_DATA_SUCCESS, { members })); 59 | } catch (error) { 60 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 61 | console.log(error); 62 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 63 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 64 | 65 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 66 | } finally { 67 | client.release(); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /functions/api/routes/room/sparkPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const alarmMessage = require('../../../constants/alarmMessage'); 5 | const responseMessage = require('../../../constants/responseMessage'); 6 | const db = require('../../../db/db'); 7 | const pushAlarm = require('../../../lib/pushAlarm'); 8 | const slackAPI = require('../../../middlewares/slackAPI'); 9 | const { userDB, roomDB, sparkDB, noticeDB } = require('../../../db'); 10 | 11 | /** 12 | * @스파크_보내기 13 | * @route POST /room/:roomId/spark 14 | * @body content: string 15 | * @error 16 | * 1. content/ roomId가 전달되지 않음 17 | * 2. 존재하지 않는 습관방 18 | * 3. 유저가 해당 습관방에 참여하지 않는 경우 19 | * 4. 쉴래요 습관완료한 사람한테 스파크 보내려함 20 | * 5. 자신에게 스파크를 보내려 할 때 21 | * 6. 해당 습관방의 record가 아닐 때 22 | */ 23 | 24 | module.exports = async (req, res) => { 25 | const { recordId, content } = req.body; 26 | const { roomId } = req.params; 27 | const user = req.user; 28 | const userId = user.userId; 29 | 30 | // @error 1. content/ roomId가 전달되지 않음 31 | if (!roomId || !content) { 32 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 33 | } 34 | 35 | let client; 36 | 37 | try { 38 | client = await db.connect(req); 39 | 40 | const room = await roomDB.getRoomById(client, roomId); 41 | // @error 2. 존재하지 않는 습관방 42 | if (!room) { 43 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ROOM_ID_INVALID)); 44 | } 45 | 46 | const entry = await roomDB.getEntryByIds(client, roomId, user.userId); 47 | 48 | // @error 3. 유저가 해당 습관방에 참여하지 않는 경우 49 | if (!entry) { 50 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_MEMBER)); 51 | } 52 | 53 | // @error 6. 해당 습관방의 record가 아닐 때 54 | const record = await roomDB.getRecordById(client, recordId); 55 | 56 | if (record.roomId !== Number(roomId)) { 57 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NOT_MATCH_ROOM_AND_RECORD)); 58 | } 59 | 60 | // @error 4. 쉴래요 습관완료한 사람한테 스파크 보내려함 61 | // if (record.status === 'DONE' || record.status === 'REST') { 62 | // return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.DONE_OR_REST_MEMBER)); 63 | // } 64 | 65 | // @error 5. 자신에게 스파크를 보내려 할 때 66 | if (record.userId === userId) { 67 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.CANNOT_SEND_SPARK_SELF)); 68 | } 69 | 70 | const spark = await sparkDB.insertSpark(client, recordId, userId, content); 71 | 72 | // 스파크를 보내면 받는 사람에게 알림 및 푸시알림 보내기 73 | const { title, body, isService, category } = alarmMessage.SEND_SPARK(user.nickname, room.roomName, content); 74 | const receiver = await userDB.getUserById(client, record.userId); 75 | await noticeDB.addNotification(client, title, body, user.profileImg, receiver.userId, isService, true, room.roomId); 76 | 77 | // 푸시알림 허용한 사용자일 경우 78 | if (receiver.pushSpark) { 79 | pushAlarm.send(req, res, title, body, receiver.deviceToken, category, null, roomId); 80 | } 81 | 82 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SEND_SPARK_SUCCESS)); 83 | } catch (error) { 84 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 85 | console.log(error); 86 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 87 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 88 | 89 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 90 | } finally { 91 | client.release(); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /functions/api/routes/scheduling/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | router.post('/inspection', require('./inspectPOST')); 5 | router.post('/remind', require('./remindPOST')); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /functions/api/routes/scheduling/inspectPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const slackAPI = require('../../../middlewares/slackAPI'); 6 | const { checkLife } = require('../../../scheduler/funcs'); 7 | 8 | /** 9 | * @인증체크_및_생명감소 10 | * @route POST /scheduling/inspection 11 | * @error 12 | */ 13 | 14 | module.exports = async (req, res) => { 15 | try { 16 | const timeLog = `[TIME STAMP] 인증 체크 시작: ${new Date()}`; 17 | console.log(timeLog); 18 | slackAPI.sendMessageToSlack(timeLog, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 19 | 20 | // 인증체크 및 생명감소 21 | await checkLife(); 22 | 23 | const slackMessage = `[🦋CERTIFICATION INSPECTION SUCCESS!🦋] [${req.method.toUpperCase()}]`; 24 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 25 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.CERTIFICATION_INSPECTION_SUCCESS)); 26 | } catch (error) { 27 | console.log(error); 28 | functions.logger.error(`[🚨CERTIFICATION INSPECTION ERROR🚨] [${req.method.toUpperCase()}]`); 29 | const slackMessage = `[🚨CERTIFICATION INSPECTION ERROR🚨] [${req.method.toUpperCase()}]`; 30 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 31 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 32 | } finally { 33 | const timeLog = `[TIME STAMP] 인증 체크 종료: ${new Date()}`; 34 | console.log(timeLog); 35 | slackAPI.sendMessageToSlack(timeLog, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /functions/api/routes/scheduling/remindPOST.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const slackAPI = require('../../../middlewares/slackAPI'); 6 | const { sendRemind } = require('../../../scheduler/funcs'); 7 | 8 | /** 9 | * @리마인드_푸시알림_전송 10 | * @route POST /scheduling/remind 11 | * @error 12 | */ 13 | 14 | module.exports = async (req, res) => { 15 | try { 16 | const timeLog = `[TIME STAMP] 리마인드 알림 시작: ${new Date()}`; 17 | console.log(timeLog); 18 | slackAPI.sendMessageToSlack(timeLog, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 19 | 20 | // 리마인드 푸시알림 전송 21 | sendRemind(); 22 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SEND_REMIND_SUCCESS)); 23 | } catch (error) { 24 | console.log(error); 25 | functions.logger.error(`[SEND REMIND ERROR] [${req.method.toUpperCase()}]`); 26 | const slackMessage = `[SEND REMIND ERROR] [${req.method.toUpperCase()}]`; 27 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 28 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 29 | } finally { 30 | const timeLog = `[TIME STAMP] 리마인드 알림 종료: ${new Date()}`; 31 | console.log(timeLog); 32 | slackAPI.sendMessageToSlack(timeLog, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /functions/api/routes/user/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { checkUser } = require('../../../middlewares/auth'); 4 | const uploadImageIntoSubDir = require('../../../middlewares/uploadImage'); 5 | 6 | router.get('/profile', checkUser, require('./userProfileGET')); 7 | router.patch('/profile', checkUser, uploadImageIntoSubDir('users'), require('./userProfilePATCH')); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /functions/api/routes/user/userProfileGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB } = require('../../../db'); 8 | 9 | /** 10 | * @프로필_조회 11 | * @route GET /user/profile 12 | * @error 13 | */ 14 | 15 | module.exports = async (req, res) => { 16 | const user = req.user; 17 | const userId = user.userId; 18 | 19 | let client; 20 | 21 | try { 22 | client = await db.connect(req); 23 | 24 | const { nickname, profileImg } = await userDB.getUserById(client, userId); 25 | 26 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_USER_PROFILE_SUCCESS, { nickname, profileImg })); 27 | } catch (error) { 28 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 29 | console.log(error); 30 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 31 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 32 | 33 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 34 | } finally { 35 | client.release(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /functions/api/routes/user/userProfilePATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const slackAPI = require('../../../middlewares/slackAPI'); 7 | const { userDB } = require('../../../db'); 8 | const { DEFAULT_PROFILE_IMG_URL } = require('../../../constants/defaultProfileImg'); 9 | 10 | /** 11 | * @프로필_변경 12 | * @route PATCH /user/profile 13 | * @error 14 | * 1. nickname이 전달되지 않음 15 | * 16 | */ 17 | 18 | module.exports = async (req, res) => { 19 | const user = req.user; 20 | const userId = user.userId; 21 | let profileImg = req.imageUrls; 22 | const { nickname } = req.body; 23 | 24 | // @error 1. 닉네임이 전달되지 않았을 경우 25 | if (!nickname) { 26 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 27 | } 28 | 29 | // 프로필 이미지가 없으면, 기본 이미지로 설정 30 | if (profileImg.length === 0) { 31 | profileImg.push(DEFAULT_PROFILE_IMG_URL); 32 | } 33 | 34 | let client; 35 | 36 | try { 37 | client = await db.connect(req); 38 | 39 | await userDB.updateProfileById(client, userId, nickname, profileImg[0]); 40 | 41 | res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.PATCH_USER_PROFILE_SUCCESS)); 42 | } catch (error) { 43 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 44 | console.log(error); 45 | const slackMessage = `[ERROR BY ${user.nickname} (${user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 46 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 47 | 48 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 49 | } finally { 50 | client.release(); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /functions/api/routes/version/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | router.get('', require('./versionGET')); 5 | router.patch('', require('./versionPATCH')); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /functions/api/routes/version/versionGET.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { versionDB } = require('../../../db'); 7 | 8 | /** 9 | * @릴리즈_버전정보_조회 10 | * @route GET /version 11 | * @error 12 | */ 13 | 14 | module.exports = async (req, res) => { 15 | let client; 16 | 17 | try { 18 | client = await db.connect(req); 19 | 20 | const { version } = await versionDB.getRecentVersion(client); 21 | 22 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.GET_RECENT_VERSION_SUCCESS, { version })); 23 | } catch (error) { 24 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 25 | 26 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 27 | } finally { 28 | client.release(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /functions/api/routes/version/versionPATCH.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const util = require('../../../lib/util'); 3 | const statusCode = require('../../../constants/statusCode'); 4 | const responseMessage = require('../../../constants/responseMessage'); 5 | const db = require('../../../db/db'); 6 | const { versionDB } = require('../../../db'); 7 | 8 | /** 9 | * @릴리즈_버전정보_갱신 10 | * @route PATCH /version 11 | * @error 12 | * 1. 신규 버전이 전달되지 않음 13 | */ 14 | 15 | module.exports = async (req, res) => { 16 | const { newVersion } = req.body; 17 | 18 | // error 1. 신규 버전이 전달되지 않음 19 | if (!newVersion) { 20 | return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); 21 | } 22 | 23 | let client; 24 | 25 | try { 26 | client = await db.connect(req); 27 | 28 | await versionDB.updateRecentVersion(client, String(newVersion)); 29 | 30 | return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.UPDATE_RECENT_VERSION_SUCCESS)); 31 | } catch (error) { 32 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 33 | 34 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 35 | } finally { 36 | client.release(); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /functions/config/dbConfig.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | let user = process.env.DEV_DB_USER; 5 | let host = process.env.DEV_DB_HOST; 6 | let database = process.env.DEV_DB_DB; 7 | let password = process.env.DEV_DB_PASSWORD; 8 | 9 | if (process.env.NODE_ENV === 'production') { 10 | user = process.env.DB_USER; 11 | host = process.env.DB_HOST; 12 | database = process.env.DB_DB; 13 | password = process.env.DB_PASSWORD; 14 | } 15 | 16 | module.exports = { 17 | user, 18 | host, 19 | database, 20 | password, 21 | }; 22 | -------------------------------------------------------------------------------- /functions/config/firebaseClient.js: -------------------------------------------------------------------------------- 1 | const { initializeApp } = require('firebase/app'); 2 | const { getAuth } = require('firebase/auth'); 3 | 4 | const firebaseConfig = { 5 | apiKey: 'AIzaSyA1TT7TFz5wJvQJS0nblrEXCag79rzyM0Y', 6 | authDomain: 'we-sopt-spark.firebaseapp.com', 7 | projectId: 'we-sopt-spark', 8 | storageBucket: 'we-sopt-spark.appspot.com', 9 | messagingSenderId: '849294133156', 10 | appId: '1:849294133156:web:dc6dc49c19dfe0e6b02331', 11 | measurementId: 'G-PWLFVZQH06', 12 | }; 13 | 14 | const firebaseApp = initializeApp(firebaseConfig); 15 | const firebaseAuth = getAuth(firebaseApp); 16 | 17 | module.exports = { firebaseApp, firebaseAuth, firebaseConfig }; 18 | -------------------------------------------------------------------------------- /functions/constants/alarmMessage.js: -------------------------------------------------------------------------------- 1 | const CERTIFICATION_COMPLETE = (who, roomName) => { 2 | const title = `${who}님의 인증 완료!`; 3 | const body = `${roomName}방 인증을 완료했어요.`; 4 | const isService = false; 5 | const category = 'certification'; 6 | 7 | return { title, body, isService, category }; 8 | }; 9 | 10 | const STATUS_CONSIDERING = (who, roomName) => { 11 | const title = `${who}님 고민중..💭`; 12 | const body = `${roomName}, 오늘 좀 힘든걸? 스파크 plz`; 13 | const isService = false; 14 | const category = 'consider'; 15 | 16 | return { title, body, isService, category }; 17 | }; 18 | 19 | const ROOM_OUT = (who, roomName) => { 20 | const title = `${roomName}방 인원 변동 🚨`; 21 | const body = `${who}님이 습관방에서 나갔어요.`; 22 | const isService = false; 23 | 24 | return { title, body, isService }; 25 | }; 26 | 27 | const SEND_SPARK = (who, roomName, content) => { 28 | const title = `${roomName}방에서 보낸 스파크`; 29 | const isService = false; 30 | const category = 'spark'; 31 | const body = `${who} : ${content}`; 32 | 33 | return { title, body, isService, category }; 34 | }; 35 | 36 | const ROOM_NEW = (roomName) => { 37 | const title = `새로운 습관 시작 🔥`; 38 | const body = `${roomName}방에서 가장 먼저 스파크를 보내볼까요?`; 39 | const isService = true; 40 | const category = 'roomStart'; 41 | 42 | return { title, body, isService, category }; 43 | }; 44 | 45 | const ROOM_DELETE = (roomName) => { 46 | const title = `${roomName} 대기방 삭제`; 47 | const body = `방 개설자에 의해 대기방이 삭제되었어요.`; 48 | const isService = true; 49 | 50 | return { title, body, isService }; 51 | }; 52 | 53 | const FEED_LIKE = (who, roomName) => { 54 | const title = `${who}님이 좋아한 피드`; 55 | const body = `${roomName}방 인증을 좋아해요.`; 56 | const isService = false; 57 | 58 | return { title, body, isService }; 59 | }; 60 | 61 | const REMIND_ALERT_NONE = (roomName) => { 62 | const title = `${roomName}방의 인증을 하지 않았어요!`; 63 | const body = `생명이 줄기 전에 서둘러 인증해주세요🏃‍♂️`; 64 | const isService = true; 65 | const category = 'remind'; 66 | 67 | return { title, body, isService, category }; 68 | }; 69 | 70 | const REMIND_ALERT_DONE = (roomName) => { 71 | const title = `${roomName}방 미인증 스파커 발견!`; 72 | const body = `지금 스파크를 보내 친구를 응원해주세요🔥`; 73 | const isService = true; 74 | const category = 'remind'; 75 | 76 | return { title, body, isService, category }; 77 | }; 78 | 79 | module.exports = { 80 | CERTIFICATION_COMPLETE, 81 | STATUS_CONSIDERING, 82 | ROOM_OUT, 83 | SEND_SPARK, 84 | ROOM_NEW, 85 | ROOM_DELETE, 86 | FEED_LIKE, 87 | REMIND_ALERT_NONE, 88 | REMIND_ALERT_DONE, 89 | }; 90 | -------------------------------------------------------------------------------- /functions/constants/day.js: -------------------------------------------------------------------------------- 1 | const numToString = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"] 2 | 3 | module.exports = { 4 | numToString 5 | }; -------------------------------------------------------------------------------- /functions/constants/defaultProfileImg.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_PROFILE_IMG_URL = 'https://firebasestorage.googleapis.com/v0/b/we-sopt-spark.appspot.com/o/common%2Fprofile_empty.png?alt=media&token=194cf154-6a1b-4ffe-9e51-6f07b3c45490'; 2 | 3 | module.exports = { 4 | DEFAULT_PROFILE_IMG_URL, 5 | }; 6 | -------------------------------------------------------------------------------- /functions/constants/jwt.js: -------------------------------------------------------------------------------- 1 | const TOKEN_EXPIRED = -3; 2 | const TOKEN_INVALID = -2; 3 | 4 | module.exports = { 5 | TOKEN_EXPIRED, 6 | TOKEN_INVALID, 7 | }; -------------------------------------------------------------------------------- /functions/constants/lifeTimelineMessage.js: -------------------------------------------------------------------------------- 1 | const LIFE_DECREASE = (count) => { 2 | const title = `생명 ${count}개 감소💧`; 3 | const content = `인증하지 않은 스파커가 있었네요. 응원이 더 필요해요!`; 4 | return { title, content }; 5 | }; 6 | 7 | const LIFE_FILL = (termDay) => { 8 | const title = `생명 충전 완료🔋`; 9 | let content = ''; 10 | 11 | if (termDay === 1) { 12 | content = '66일의 도전을 시작했네요. 인증하고 생명을 지켜요!'; 13 | } else if (termDay === 4) { 14 | content = '3일 달성 선물로 생명이 충전됐어요. 잘하고 있어요!'; 15 | } else if (termDay === 8) { 16 | content = '7일 달성 선물로 생명이 충전됐어요. 더 힘내봐요!'; 17 | } else if (termDay === 34) { 18 | content = '33일 달성 선물로 생명이 충전됐어요. 할 수 있어요!'; 19 | } else if (termDay === 60) { 20 | content = '마지막 7일 선물로 생명이 충전됐어요. 끝까지 힘내요!'; 21 | } 22 | 23 | return { title, content }; 24 | }; 25 | 26 | module.exports = { 27 | LIFE_DECREASE, 28 | LIFE_FILL, 29 | }; 30 | -------------------------------------------------------------------------------- /functions/constants/responseMessage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NULL_VALUE: '필요한 값이 없습니다', 3 | OUT_OF_VALUE: '파라미터 값이 잘못되었습니다', 4 | PATH_ERROR: '요청 경로가 올바르지 않습니다', 5 | INTERNAL_SERVER_ERROR: '서버 내부 오류', 6 | PRIV_NOT_FOUND: '권한이 없는 요청입니다', 7 | INVALID_LASTID: '잘못된 lastId 입니다', 8 | 9 | // 회원가입 10 | CREATED_USER: '회원 가입 성공', 11 | DELETE_USER: '회원 탈퇴 성공', 12 | ALREADY_SOCIALID: '이미 사용중인 소셜 아이디입니다.', 13 | TOO_LONG_NICKNAME: '닉네임은 10자를 초과할 수 없습니다', 14 | NOT_SIGNED_UP: '회원가입을 하지 않은 사용자입니다', 15 | ALREADY_SIGNED_UP: '회원 정보를 불러왔습니다', 16 | 17 | // 로그인 18 | LOGIN_SUCCESS: '로그인 성공', 19 | LOGIN_FAIL: '로그인 실패', 20 | NO_USER: '존재하지 않는 회원입니다.', 21 | MISS_MATCH_PW: '비밀번호가 맞지 않습니다.', 22 | 23 | // 회원 탈퇴 24 | ALREADY_DELETED_USER: '이미 탈퇴한 회원입니다.', 25 | DELETE_USER_SUCCESS: '회원 탈퇴 성공', 26 | 27 | // 로그아웃 28 | LOGOUT_SUCCESS: '로그아웃 성공', 29 | 30 | // 사용자 프로필 31 | GET_USER_PROFILE_SUCCESS: '프로필 조회 성공', 32 | PATCH_USER_PROFILE_SUCCESS: '프로필 변경 성공', 33 | 34 | // Room 35 | CREATE_ROOM_SUCCESS: '습관 방 생성 성공', 36 | CREATE_ROOM_FAIL: '습관 방 생성 실패', 37 | GET_WAITROOM_DATA_SUCCESS: '대기방 정보 확인 완료', 38 | GET_WAITROOM_DATA_IMPOSSIBLE: '참여할 수 없는 코드예요.', 39 | GET_WAITROOM_DATA_NULL: '참여할 수 없는 코드예요.', 40 | GET_WAITROOM_DATA_STARTED: '이미 시작된 습관방이에요.', 41 | GET_WAITROOM_DATA_ALREADY: '이미 참여중인 코드예요.', 42 | GET_WAITROOM_DATA_FULL: '인원초과로 참여할 수 없어요.', 43 | ENTER_ROOM_SUCCESS: '습관 방 참여 완료', 44 | ENTER_ROOM_FAIL: '습관 방 참여 실패', 45 | ENTER_ROOM_ALREADY: '이미 참여중인 습관 방입니다', 46 | ROOM_ID_INVALID: '올바르지 않은 roomId입니다', 47 | PURPOSE_SET_SUCCESS: '목표 설정 성공', 48 | ROOM_ID_NOT_FOUND: '유효하지 않은 roomId입니다', 49 | GET_ROOM_DATA_FAIL: '존재하지 않는 습관방입니다', 50 | NOT_ONGOING_ROOM: '현재 진행중인 습관방이 아닙니다', 51 | NOT_MEMBER: '참여중인 습관방이 아닙니다', 52 | GET_ROOM_LIST_SUCCESS: '참여중인 습관방 조회 완료', 53 | GET_ROOM_DETAIL_SUCCESS: '특정 습관방 상세조회 성공', 54 | NOT_MATCH_ROOM_AND_RECORD: '해당 습관방의 record가 아닙니다', 55 | NOT_STARTED_ROOM: '아직 대기중인 방입니다', 56 | ROOM_NOT_WAITING: '대기중인 습관방이 아닙니다', 57 | INCORRECT_RECORD: '올바르지 않은 recordId 입니다', 58 | GET_THUMBNAIL_LIST_SUCCESS: '보관함 대표사진 변경 뷰 사진 불러오기 성공', 59 | UPDATE_THUMBNAIL_SUCCESS: '보관함 대표사진 변경 성공', 60 | 61 | START_ROOM_SUCCESS: '습관 방 시작 완료', 62 | START_ROOM_ALREADY: '이미 시작된 방입니다', 63 | DONE_OR_REST_MEMBER: '습관 인증 완료 혹은 쉴래요 한 사용자입니다', 64 | CERTIFY_SUCCESS: '습관인증 업로드 성공', 65 | ROOM_OUT_SUCCESS: '습관 방 퇴장 완료', 66 | ROOM_DELETE_SUCCESS: '대기방 삭제 완료', 67 | HOST_WAITROOM_OUT_FAIL: '방 생성자는 대기방을 나갈 수 없습니다', 68 | DIALOG_READ_SUCCESS: '성공/실패한 습관방 읽음처리 완료', 69 | 70 | // Life Timeline 71 | GET_LIFE_TIMELINE_SUCCESS: '생명 타임라인 조회 성공', 72 | LIFE_TIMELINE_READ_SUCCESS: '생명 타임라인 읽음처리 완료', 73 | 74 | // Feed 75 | GET_FEED_SUCCESS: '피드 조회 성공', 76 | RECORD_ID_NOT_VALID: '유효하지 않은 recordId입니다', 77 | SEND_LIKE_SUCCESS: '좋아요 성공', 78 | CANCEL_LIKE_SUCCESS: '좋아요 취소 성공', 79 | REPORT_FEED_SUCCESS: '피드 신고 성공', 80 | 81 | // Spark 82 | CANNOT_SEND_SPARK_SELF: '자기자신에게 스파크를 보낼 수 없습니다', 83 | SEND_SPARK_SUCCESS: '스파크 전송 선공', 84 | 85 | // Myroom 86 | GET_MYROOM_SUCCESS: '보관함 리스트 불러오기 성공', 87 | GET_MYROOM_DETAIL_SUCCESS: '인증사진 모아보기 성공', 88 | 89 | // Notice 90 | SERVICE_READ_SUCCESS: '서비스 알림 읽음처리 완료', 91 | ACTIVE_READ_SUCCESS: '활동 알림 읽음처리 완료', 92 | GET_SERVICE_SUCCESS: '서비스 알림 조회 완료', 93 | GET_ACTIVE_SUCCESS: '활동 알림 조회 완료', 94 | NOTICE_DELETE_SUCCESS: '알림 삭제 완료', 95 | NOTICE_ID_NOT_VALID: '유효하지 않은 알림 ID 입니다', 96 | PUSH_SEND_SUCCESS: '푸시알림 전송 완료', 97 | GET_NEW_NOTICE_SUCCESS: '새로운 알림 조회 완료', 98 | GET_NOTICE_SETTING_SUCCESS: '푸시알림 설정 조회 완료', 99 | PUSH_CATEGORY_INVALID: '유효하지 않은 category입니다', 100 | PUSH_TOGGLE_SUCCESS: '푸시알림 설정 변경 완료', 101 | 102 | // Status 103 | INVALID_USER_STATUS: '유효하지 않은 status type입니다', 104 | CERTIFICATION_ALREADY_DONE: '이미 인증을 완료하였습니다', 105 | REST_ALREADY_DONE: '이미 쉴래요를 사용한 사용자입니다', 106 | CONSIDER_ALREADY_DONE: '이미 고민중 상태의 사용자입니다', 107 | UPDATE_STATUS_SUCCESS: '상태 변경 완료', 108 | REST_COUNT_ZERO: '쉴래요 사용 가능 횟수가 0인 사용자입니다', 109 | 110 | // Token 111 | TOKEN_EXPIRED: '만료된 토큰입니다', 112 | TOKEN_INVALID: '유효하지 않은 토큰입니다', 113 | 114 | // Scheduling 115 | CERTIFICATION_INSPECTION_SUCCESS: '인증 검사 성공', 116 | SEND_REMIND_SUCCESS: '리마인드 알림 보내기 성공', 117 | 118 | // Version 119 | GET_RECENT_VERSION_SUCCESS: '최신 버전정보 불러오기 성공', 120 | UPDATE_RECENT_VERSION_SUCCESS: '최신 버전정보 갱신 성공', 121 | }; 122 | -------------------------------------------------------------------------------- /functions/constants/statusCode.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OK: 200, 3 | CREATED: 201, 4 | NO_CONTENT: 204, 5 | BAD_REQUEST: 400, 6 | UNAUTHORIZED: 401, 7 | FORBIDDEN: 403, 8 | NOT_FOUND: 404, 9 | CONFLICT: 409, 10 | INTERNAL_SERVER_ERROR: 500, 11 | SERVICE_UNAVAILABLE: 503, 12 | DB_ERROR: 600, 13 | }; 14 | -------------------------------------------------------------------------------- /functions/constants/termList.js: -------------------------------------------------------------------------------- 1 | const termList = [1, 4, 8, 34, 60]; 2 | 3 | module.exports = { 4 | termList, 5 | }; 6 | -------------------------------------------------------------------------------- /functions/db/db.js: -------------------------------------------------------------------------------- 1 | // 필요한 모듈들 2 | const functions = require('firebase-functions'); 3 | const { Pool, Query } = require('pg'); 4 | const dayjs = require('dayjs'); 5 | const dotenv = require('dotenv'); 6 | 7 | dotenv.config(); 8 | 9 | // DB Config (유저, 호스트, DB 이름, 패스워드)를 로딩해줍시다. 10 | const dbConfig = require('../config/dbConfig'); 11 | 12 | // NODE_ENV라는 글로벌 환경변수를 사용해서, 현재 환경이 어떤 '모드'인지 판별해줍시다. 13 | let devMode = process.env.NODE_ENV === 'development'; 14 | 15 | // SQL 쿼리문을 콘솔에 프린트할지 말지 결정해주는 변수를 선언합시다. 16 | const sqlDebug = true; 17 | 18 | // 기본 설정에서는 우리가 실행하게 되는 SQL 쿼리문이 콘솔에 찍히지 않기 때문에, 19 | // pg 라이브러리 내부의 함수를 살짝 손봐서 SQL 쿼리문이 콘솔에 찍히게 만들어 줍시다. 20 | const submit = Query.prototype.submit; 21 | Query.prototype.submit = function () { 22 | const text = this.text; 23 | const values = this.values || []; 24 | const query = text.replace(/\$([0-9]+)/g, (m, v) => JSON.stringify(values[parseInt(v) - 1])); 25 | // devMode === true 이면서 sqlDebug === true일 때 SQL 쿼리문을 콘솔에 찍겠다는 분기입니다. 26 | devMode && sqlDebug && console.log(`\n\n[👻 SQL STATEMENT]\n${query}\n_________\n`); 27 | submit.apply(this, arguments); 28 | }; 29 | 30 | // 서버가 실행되면 현재 환경이 개발 모드(로컬)인지 프로덕션 모드(배포)인지 콘솔에 찍어줍시다. 31 | console.log(`[🔥DB] ${process.env.NODE_ENV} / DB: ${dbConfig.database}`); 32 | 33 | // 커넥션 풀을 생성해줍니다. 34 | const pool = new Pool({ 35 | ...dbConfig, 36 | connectionTimeoutMillis: 60 * 1000, 37 | idleTimeoutMillis: 60 * 1000, 38 | }); 39 | 40 | // 위에서 생성한 커넥션 풀에서 커넥션을 빌려오는 함수를 정의합니다. 41 | // 기본적으로 제공되는 pool.connect()와 pool.connect().release() 함수에 디버깅용 메시지를 추가하는 작업입니다. 42 | const connect = async (req) => { 43 | const now = dayjs(); 44 | const string = 45 | !!req && !!req.method 46 | ? `[${req.method}] ${!!req.user ? `${req.user.id}` : ``} ${req.originalUrl}\n ${!!req.query && `query: ${JSON.stringify(req.query)}`} ${!!req.body && `body: ${JSON.stringify(req.body)}`} ${ 47 | !!req.params && `params ${JSON.stringify(req.params)}` 48 | }` 49 | : `request 없음`; 50 | const callStack = new Error().stack; 51 | const client = await pool.connect(); 52 | const query = client.query; 53 | const release = client.release; 54 | 55 | const releaseChecker = setTimeout(() => { 56 | devMode 57 | ? console.error('[ERROR] client connection이 15초 동안 릴리즈되지 않았습니다.', { callStack }) 58 | : functions.logger.error('[ERROR] client connection이 15초 동안 릴리즈되지 않았습니다.', { callStack }); 59 | devMode ? console.error(`마지막으로 실행된 쿼리문입니다. ${client.lastQuery}`) : functions.logger.error(`마지막으로 실행된 쿼리문입니다. ${client.lastQuery}`); 60 | }, 15 * 1000); 61 | 62 | client.query = (...args) => { 63 | client.lastQuery = args; 64 | return query.apply(client, args); 65 | }; 66 | client.release = () => { 67 | clearTimeout(releaseChecker); 68 | const time = dayjs().diff(now, 'millisecond'); 69 | if (time > 4000) { 70 | const message = `[RELEASE] in ${time} | ${string}`; 71 | devMode && console.log(message); 72 | } 73 | client.query = query; 74 | client.release = release; 75 | return release.apply(client); 76 | }; 77 | return client; 78 | }; 79 | 80 | module.exports = { 81 | connect, 82 | }; 83 | -------------------------------------------------------------------------------- /functions/db/dialog.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const insertDialogs = async (client, dialogs) => { 6 | const { rows } = await client.query( 7 | ` 8 | INSERT INTO spark.dialog 9 | (user_id, room_id, type, date) 10 | VALUES 11 | ${dialogs.join()} 12 | RETURNING * 13 | `, 14 | ); 15 | 16 | return convertSnakeToCamel.keysToCamel(rows); 17 | }; 18 | 19 | const getUserDialogs = async (client, userId, types) => { 20 | const { rows } = await client.query( 21 | ` 22 | SELECT * 23 | FROM spark.dialog d 24 | INNER JOIN spark.room r 25 | ON d.room_id = r.room_id 26 | WHERE user_id = $1 27 | AND type IN (${types.join()}) 28 | AND is_read = FALSE 29 | `, 30 | [userId], 31 | ); 32 | return convertSnakeToCamel.keysToCamel(rows); 33 | }; 34 | 35 | const setDialogRead = async (client, dialogId) => { 36 | const now = dayjs().add(9, 'h'); 37 | const { rows } = await client.query( 38 | ` 39 | UPDATE spark.dialog 40 | SET is_read = TRUE, updated_at = $2, read_at = $2 41 | WHERE dialog_id = $1 42 | RETURNING * 43 | `, 44 | [dialogId, now], 45 | ); 46 | 47 | return convertSnakeToCamel.keysToCamel(rows[0]); 48 | }; 49 | 50 | const setLifeDeductionDialogsRead = async (client, userId, roomId) => { 51 | const now = dayjs().add(9, 'hour'); 52 | const { rows } = await client.query( 53 | ` 54 | UPDATE spark.dialog 55 | SET is_read = TRUE, updated_at = $3, read_at = $3 56 | WHERE user_id = $1 57 | AND room_id = $2 58 | AND is_read = FALSE 59 | AND type = 'LIFE_DEDUCTION' 60 | RETURNING * 61 | `, 62 | [userId, roomId, now], 63 | ); 64 | return convertSnakeToCamel.keysToCamel(rows); 65 | }; 66 | 67 | const getUnReadDialogByRoomAndUser = async (client, roomId, userId) => { 68 | const { rows } = await client.query( 69 | ` 70 | SELECT * 71 | FROM spark.dialog 72 | WHERE user_id = $2 73 | AND room_id = $1 74 | AND is_read = FALSE 75 | AND type IN ('COMPLETE', 'FAIL') 76 | `, 77 | [roomId, userId], 78 | ); 79 | return convertSnakeToCamel.keysToCamel(rows[0]); 80 | }; 81 | 82 | module.exports = { 83 | insertDialogs, 84 | getUserDialogs, 85 | setDialogRead, 86 | getUnReadDialogByRoomAndUser, 87 | setLifeDeductionDialogsRead, 88 | }; 89 | -------------------------------------------------------------------------------- /functions/db/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | userDB: require('./user'), 3 | roomDB: require('./room'), 4 | sparkDB: require('./spark'), 5 | likeDB: require('./like'), 6 | noticeDB: require('./notice'), 7 | recordDB: require('./record'), 8 | scheduleDB: require('./schedule'), 9 | reportDB: require('./report'), 10 | dialogDB: require('./dialog'), 11 | remindDB: require('./remind'), 12 | ownershipDB: require('./ownership'), 13 | versionDB: require('./version'), 14 | lifeTimelineDB: require('./lifeTimeline'), 15 | }; 16 | -------------------------------------------------------------------------------- /functions/db/lifeTimeline.js: -------------------------------------------------------------------------------- 1 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 2 | 3 | const addDecreaseTimelines = async (client, timelines) => { 4 | const { rows } = await client.query( 5 | ` 6 | INSERT INTO spark.life_timeline 7 | (receiver_id, room_id, is_decrease, decrease_count, profile_1, profile_2) 8 | VALUES 9 | ${timelines.join()} 10 | RETURNING * 11 | `, 12 | ); 13 | return convertSnakeToCamel.keysToCamel(rows[0]); 14 | }; 15 | 16 | const addFillTimelines = async (client, timelines) => { 17 | const { rows } = await client.query( 18 | ` 19 | INSERT INTO spark.life_timeline 20 | (receiver_id, room_id, is_decrease, term_day) 21 | VALUES 22 | ${timelines.join()} 23 | RETURNING * 24 | `, 25 | ); 26 | return convertSnakeToCamel.keysToCamel(rows[0]); 27 | }; 28 | 29 | const getLifeTimeline = async (client, roomId, userId) => { 30 | const { rows } = await client.query( 31 | ` 32 | SELECT * FROM spark.life_timeline 33 | WHERE room_id = $1 34 | AND receiver_id = $2 35 | ORDER BY life_timeline_id DESC 36 | `, 37 | [roomId, userId], 38 | ); 39 | return convertSnakeToCamel.keysToCamel(rows); 40 | }; 41 | 42 | const readLifeTimeline = async (client, roomId, userId) => { 43 | const { rows } = await client.query( 44 | ` 45 | UPDATE spark.life_timeline 46 | SET is_read = TRUE 47 | WHERE room_id = $1 48 | AND receiver_id = $2 49 | RETURNING * 50 | `, 51 | [roomId, userId], 52 | ); 53 | return convertSnakeToCamel.keysToCamel(rows); 54 | }; 55 | 56 | const getNewTimelineCount = async (client, roomId, userId) => { 57 | const { rows } = await client.query( 58 | ` 59 | SELECT count(*) as number FROM spark.life_timeline 60 | WHERE room_id = $1 61 | AND receiver_id = $2 62 | AND is_read = FALSE 63 | `, 64 | [roomId, userId], 65 | ); 66 | return convertSnakeToCamel.keysToCamel(rows[0].number); 67 | }; 68 | 69 | module.exports = { 70 | addDecreaseTimelines, 71 | addFillTimelines, 72 | getLifeTimeline, 73 | readLifeTimeline, 74 | getNewTimelineCount, 75 | }; 76 | -------------------------------------------------------------------------------- /functions/db/like.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const countLikeByRecordId = async (client, recordId) => { 6 | const { rows } = await client.query( 7 | ` 8 | SELECT COUNT(*) 9 | FROM spark.like 10 | WHERE record_id = $1 11 | `, 12 | [recordId], 13 | ); 14 | return convertSnakeToCamel.keysToCamel(rows); 15 | }; 16 | 17 | const checkIsLike = async (client, recordId, userId) => { 18 | const { rows } = await client.query( 19 | ` 20 | SELECT * FROM spark.like 21 | WHERE record_id = $1 22 | AND sender_id = $2 23 | `, 24 | [recordId, userId], 25 | ); 26 | return convertSnakeToCamel.keysToCamel(rows[0]); 27 | }; 28 | 29 | const sendLike = async (client, recordId, senderId) => { 30 | const now = dayjs().add(9, 'hour'); 31 | const { rows } = await client.query( 32 | ` 33 | INSERT INTO spark.like 34 | (record_id, sender_id, created_at) 35 | VALUES 36 | ($1, $2, $3) 37 | RETURNING * 38 | `, 39 | [recordId, senderId, now], 40 | ); 41 | 42 | return convertSnakeToCamel.keysToCamel(rows[0]); 43 | }; 44 | 45 | const cancelLike = async (client, recordId, senderId) => { 46 | const { rows } = await client.query( 47 | ` 48 | DELETE FROM spark.like 49 | WHERE record_id = $1 50 | AND sender_id = $2 51 | RETURNING * 52 | `, 53 | [recordId, senderId], 54 | ); 55 | 56 | return convertSnakeToCamel.keysToCamel(rows[0]); 57 | }; 58 | 59 | module.exports = { 60 | countLikeByRecordId, 61 | checkIsLike, 62 | sendLike, 63 | cancelLike, 64 | }; 65 | -------------------------------------------------------------------------------- /functions/db/notice.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const serviceReadByUserId = async (client, userId) => { 6 | const now = dayjs().add(9, 'hour'); 7 | const { rows } = await client.query( 8 | ` 9 | UPDATE spark.notification 10 | SET is_read = TRUE, read_at = $2, updated_at = $2 11 | WHERE is_read = FALSE 12 | AND is_deleted = FALSE 13 | AND is_service = TRUE 14 | AND receiver_id = $1 15 | RETURNING * 16 | `, 17 | [userId, now], 18 | ); 19 | return convertSnakeToCamel.keysToCamel(rows); 20 | }; 21 | 22 | const activeReadByUserId = async (client, userId) => { 23 | const now = dayjs().add(9, 'hour'); 24 | const { rows } = await client.query( 25 | ` 26 | UPDATE spark.notification 27 | SET is_read = TRUE, read_at = $2, updated_at = $2 28 | WHERE is_read = FALSE 29 | AND is_deleted = FALSE 30 | AND is_service = FALSE 31 | AND receiver_id = $1 32 | RETURNING * 33 | `, 34 | [userId, now], 35 | ); 36 | return convertSnakeToCamel.keysToCamel(rows); 37 | }; 38 | 39 | const getNoticeByNoticeId = async (client, noticeId) => { 40 | const { rows } = await client.query( 41 | ` 42 | SELECT * FROM spark.notification 43 | WHERE notification_id = $1 44 | `, 45 | [noticeId], 46 | ); 47 | return convertSnakeToCamel.keysToCamel(rows[0]); 48 | }; 49 | 50 | const deleteNoticeByNoticeId = async (client, noticeId) => { 51 | const now = dayjs().add(9, 'h'); 52 | const { rows } = await client.query( 53 | ` 54 | UPDATE spark.notification 55 | SET is_deleted = TRUE, deleted_at = $2, updated_at = $2 56 | WHERE notification_id = $1 57 | RETURNING * 58 | `, 59 | [noticeId, now], 60 | ); 61 | return convertSnakeToCamel.keysToCamel(rows[0]); 62 | }; 63 | 64 | const getServicesByUserId = async (client, userId, lastId, size) => { 65 | if (lastId === -1) { 66 | const { rows } = await client.query( 67 | ` 68 | SELECT * FROM spark.notification 69 | WHERE receiver_id = $1 70 | AND room_id in ( 71 | SELECT room_id 72 | FROM spark.entry 73 | WHERE user_id = $1 74 | AND is_out = FALSE 75 | AND is_kicked = FALSE 76 | ) 77 | AND is_deleted = FALSE 78 | AND is_service = TRUE 79 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 80 | ORDER BY notification_id DESC 81 | LIMIT $2 82 | `, 83 | [userId, size], 84 | ); 85 | return convertSnakeToCamel.keysToCamel(rows); 86 | } else { 87 | const { rows } = await client.query( 88 | ` 89 | SELECT * FROM spark.notification 90 | WHERE receiver_id = $1 91 | AND room_id in ( 92 | SELECT room_id 93 | FROM spark.entry 94 | WHERE user_id = $1 95 | AND is_out = FALSE 96 | AND is_kicked = FALSE 97 | ) 98 | AND is_deleted = FALSE 99 | AND is_service = TRUE 100 | AND notification_id < $2 101 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 102 | ORDER BY notification_id DESC 103 | LIMIT $3 104 | `, 105 | [userId, lastId, size], 106 | ); 107 | return convertSnakeToCamel.keysToCamel(rows); 108 | } 109 | }; 110 | 111 | const getActivesByUserId = async (client, userId, lastId, size) => { 112 | if (lastId === -1) { 113 | const { rows } = await client.query( 114 | ` 115 | SELECT * FROM spark.notification 116 | WHERE receiver_id = $1 117 | AND room_id in ( 118 | SELECT room_id 119 | FROM spark.entry 120 | WHERE user_id = $1 121 | AND is_out = FALSE 122 | AND is_kicked = FALSE 123 | ) 124 | AND is_deleted = FALSE 125 | AND is_service = FALSE 126 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 127 | ORDER BY notification_id DESC 128 | LIMIT $2 129 | `, 130 | [userId, size], 131 | ); 132 | return convertSnakeToCamel.keysToCamel(rows); 133 | } else { 134 | const { rows } = await client.query( 135 | ` 136 | SELECT * FROM spark.notification 137 | WHERE receiver_id = $1 138 | AND room_id in ( 139 | SELECT room_id 140 | FROM spark.entry 141 | WHERE user_id = $1 142 | AND is_out = FALSE 143 | AND is_kicked = FALSE 144 | ) 145 | AND is_deleted = FALSE 146 | AND is_service = FALSE 147 | AND notification_id < $2 148 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 149 | ORDER BY notification_id DESC 150 | LIMIT $3 151 | `, 152 | [userId, lastId, size], 153 | ); 154 | return convertSnakeToCamel.keysToCamel(rows); 155 | } 156 | }; 157 | 158 | const addNotification = async (client, title, body, thumbnail, receiverId, isService, isThumbProfile, roomId) => { 159 | const { rows } = await client.query( 160 | ` 161 | INSERT INTO spark.notification 162 | (title, content, thumbnail, receiver_id, is_service, is_thumb_profile, room_id) 163 | VALUES 164 | ($1, $2, $3, $4, $5, $6, $7) 165 | RETURNING * 166 | `, 167 | [title, body, thumbnail, receiverId, isService, isThumbProfile, roomId], 168 | ); 169 | return convertSnakeToCamel.keysToCamel(rows[0]); 170 | }; 171 | 172 | const addNotifications = async (client, notifications) => { 173 | const { rows } = await client.query( 174 | ` 175 | INSERT INTO spark.notification 176 | (title, content, thumbnail, receiver_id, is_service, is_thumb_profile, room_id) 177 | VALUES 178 | ${notifications.join()} 179 | RETURNING * 180 | `, 181 | ); 182 | return convertSnakeToCamel.keysToCamel(rows[0]); 183 | }; 184 | 185 | const getNumberOfUnreadNoticeById = async (client, userId) => { 186 | const { rows } = await client.query( 187 | ` 188 | SELECT count(*) as number FROM spark.notification 189 | WHERE receiver_id = $1 190 | AND room_id in ( 191 | SELECT room_id 192 | FROM spark.entry 193 | WHERE user_id = $1 194 | AND is_out = FALSE 195 | AND is_kicked = FALSE 196 | ) 197 | AND is_deleted = FALSE 198 | AND is_read = FALSE 199 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 200 | `, 201 | [userId], 202 | ); 203 | return convertSnakeToCamel.keysToCamel(rows[0].number); 204 | }; 205 | 206 | const getNumberOfUnreadServiceNoticeById = async (client, userId) => { 207 | const { rows } = await client.query( 208 | ` 209 | SELECT count(*) as number FROM spark.notification 210 | WHERE receiver_id = $1 211 | AND room_id in ( 212 | SELECT room_id 213 | FROM spark.entry 214 | WHERE user_id = $1 215 | AND is_out = FALSE 216 | AND is_kicked = FALSE 217 | ) 218 | AND is_deleted = FALSE 219 | AND is_read = FALSE 220 | AND is_service = TRUE 221 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 222 | `, 223 | [userId], 224 | ); 225 | return convertSnakeToCamel.keysToCamel(rows[0].number); 226 | }; 227 | 228 | const getNumberOfUnreadActiveNoticeById = async (client, userId) => { 229 | const { rows } = await client.query( 230 | ` 231 | SELECT count(*) as number FROM spark.notification 232 | WHERE receiver_id = $1 233 | AND room_id in ( 234 | SELECT room_id 235 | FROM spark.entry 236 | WHERE user_id = $1 237 | AND is_out = FALSE 238 | AND is_kicked = FALSE 239 | ) 240 | AND is_deleted = FALSE 241 | AND is_read = FALSE 242 | AND is_service = FALSE 243 | AND created_at >= (CURRENT_TIMESTAMP - INTERVAL '7 days - 9 hours')::date 244 | `, 245 | [userId], 246 | ); 247 | return convertSnakeToCamel.keysToCamel(rows[0].number); 248 | }; 249 | 250 | const deleteNoticeByContentReceiverAndThumbnail = async (client, title, body, isService, receiverId, thumbnail) => { 251 | const { rows } = await client.query( 252 | ` 253 | DELETE FROM spark.notification 254 | WHERE title = $1 255 | AND content = $2 256 | AND is_service = $3 257 | AND receiver_id = $4 258 | AND thumbnail = $5 259 | RETURNING * 260 | `, 261 | [title, body, isService, receiverId, thumbnail], 262 | ); 263 | return convertSnakeToCamel.keysToCamel(rows[0]); 264 | }; 265 | 266 | module.exports = { 267 | serviceReadByUserId, 268 | activeReadByUserId, 269 | getNoticeByNoticeId, 270 | deleteNoticeByNoticeId, 271 | getServicesByUserId, 272 | getActivesByUserId, 273 | addNotification, 274 | addNotifications, 275 | getNumberOfUnreadNoticeById, 276 | getNumberOfUnreadServiceNoticeById, 277 | getNumberOfUnreadActiveNoticeById, 278 | deleteNoticeByContentReceiverAndThumbnail, 279 | }; 280 | -------------------------------------------------------------------------------- /functions/db/ownership.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 3 | 4 | const insertOwnership = async (client, userId, filePath) => { 5 | const now = dayjs().add(9, 'hour'); 6 | const { rows } = await client.query( 7 | ` 8 | INSERT INTO spark.ownership 9 | (user_id, file_path, created_at) 10 | VALUES 11 | ($1, $2, $3) 12 | RETURNING * 13 | `, 14 | [userId, filePath, now], 15 | ); 16 | return convertSnakeToCamel.keysToCamel(rows[0]); 17 | }; 18 | 19 | const deleteAllOwnershipByUserId = async (client, userId) => { 20 | const now = dayjs().add(9, 'hour'); 21 | const { rows } = await client.query( 22 | ` 23 | UPDATE spark.ownership 24 | SET file_deleted = true, file_deleted_at = $2 25 | WHERE user_id = $1 26 | RETURNING * 27 | `, 28 | [userId, now], 29 | ); 30 | return convertSnakeToCamel.keysToCamel(rows); 31 | }; 32 | 33 | const getOwnershipByUserId = async (client, userId) => { 34 | const { rows } = await client.query( 35 | ` 36 | SELECT * FROM spark.ownership 37 | WHERE user_id = $1 38 | AND file_deleted = FALSE 39 | `, 40 | [userId], 41 | ); 42 | return convertSnakeToCamel.keysToCamel(rows); 43 | }; 44 | 45 | module.exports = { 46 | insertOwnership, 47 | deleteAllOwnershipByUserId, 48 | getOwnershipByUserId, 49 | }; 50 | -------------------------------------------------------------------------------- /functions/db/record.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const getRecordById = async (client, recordId) => { 6 | const { rows } = await client.query( 7 | ` 8 | SELECT * FROM spark.record 9 | WHERE record_id = $1 10 | `, 11 | [recordId], 12 | ); 13 | return convertSnakeToCamel.keysToCamel(rows[0]); 14 | }; 15 | 16 | const getRecentRecordByEntryId = async (client, entryId) => { 17 | const { rows } = await client.query( 18 | ` 19 | SELECT * FROM spark.record 20 | WHERE entry_id = $1 21 | ORDER BY record_id desc 22 | LIMIT 1 23 | `, 24 | [entryId], 25 | ); 26 | return convertSnakeToCamel.keysToCamel(rows[0]); 27 | }; 28 | 29 | const updateStatusByRecordId = async (client, recordId, statusType) => { 30 | const now = dayjs().add(9, 'hour'); 31 | const { rows } = await client.query( 32 | ` 33 | UPDATE spark.record 34 | SET status = $2, updated_at = $3 35 | WHERE record_id = $1 36 | RETURNING * 37 | `, 38 | [recordId, statusType, now], 39 | ); 40 | return convertSnakeToCamel.keysToCamel(rows[0]); 41 | }; 42 | 43 | const uploadRecord = async (client, recordId, certifyingImg, timerRecord) => { 44 | const now = dayjs().add(9, 'hour'); 45 | const { rows } = await client.query( 46 | ` 47 | UPDATE spark.record 48 | SET certifying_img = $2, timer_record = $3, status = 'DONE', updated_at = $4, certified_at = $4 49 | WHERE record_id = $1 50 | RETURNING * 51 | `, 52 | [recordId, certifyingImg, timerRecord, now], 53 | ); 54 | return convertSnakeToCamel.keysToCamel(rows[0]); 55 | }; 56 | 57 | const getPagedRecordsByEntryId = async (client, entryId, lastId, size) => { 58 | const now = dayjs().add(9, 'hour'); 59 | const today = dayjs(now).format('YYYY-MM-DD'); 60 | if (lastId === -1) { 61 | const { rows } = await client.query( 62 | ` 63 | SELECT * 64 | FROM spark.record r 65 | WHERE r.entry_id = $1 66 | AND r.day != 0 67 | AND (r.status in ('DONE', 'REST') OR r.date != $3) 68 | ORDER BY r.day DESC 69 | LIMIT $2 70 | `, 71 | [entryId, size, today], 72 | ); 73 | return convertSnakeToCamel.keysToCamel(rows); 74 | } else { 75 | const { rows } = await client.query( 76 | ` 77 | SELECT * 78 | FROM spark.record r 79 | WHERE r.entry_id = $1 80 | AND r.record_id < $2 81 | AND (r.status in ('DONE', 'REST') OR r.date != $4) 82 | ORDER BY r.day DESC 83 | LIMIT $3 84 | `, 85 | [entryId, lastId, size, today], 86 | ); 87 | return convertSnakeToCamel.keysToCamel(rows); 88 | } 89 | }; 90 | 91 | const getDonePagedRecordsByEntryId = async (client, entryId, lastId, size) => { 92 | const now = dayjs().add(9, 'hour'); 93 | const today = dayjs(now).format('YYYY-MM-DD'); 94 | if (lastId === -1) { 95 | const { rows } = await client.query( 96 | ` 97 | SELECT * 98 | FROM spark.record r 99 | WHERE r.entry_id = $1 100 | AND r.day != 0 101 | AND r.status = 'DONE' 102 | ORDER BY r.day DESC 103 | LIMIT $2 104 | `, 105 | [entryId, size], 106 | ); 107 | return convertSnakeToCamel.keysToCamel(rows); 108 | } else { 109 | const { rows } = await client.query( 110 | ` 111 | SELECT * 112 | FROM spark.record r 113 | WHERE r.entry_id = $1 114 | AND r.record_id < $2 115 | AND r.status = 'DONE' 116 | ORDER BY r.day DESC 117 | LIMIT $3 118 | `, 119 | [entryId, lastId, size], 120 | ); 121 | return convertSnakeToCamel.keysToCamel(rows); 122 | } 123 | }; 124 | 125 | const insertRecords = async (client, insertEntries) => { 126 | const { rows } = await client.query( 127 | ` 128 | INSERT INTO spark.record 129 | (entry_id, date, day) 130 | VALUES 131 | ${insertEntries.join()} 132 | RETURNING * 133 | `, 134 | ); 135 | 136 | return convertSnakeToCamel.keysToCamel(rows); 137 | }; 138 | 139 | const getNoneOrConsiderEntryIdsByDate = async (client, date) => { 140 | const { rows } = await client.query( 141 | ` 142 | SELECT entry_id FROM spark.record 143 | WHERE date = $1 144 | AND status IN ('NONE', 'CONSIDER') 145 | `, 146 | [date], 147 | ); 148 | return convertSnakeToCamel.keysToCamel(rows); 149 | }; 150 | 151 | const getPushRemindUsers = async (client, date) => { 152 | const { rows } = await client.query( 153 | ` 154 | SELECT e.user_id, r.room_name, r.room_id, rec.status, u.device_token 155 | FROM spark.entry e 156 | INNER JOIN spark.room r 157 | ON e.room_id = r.room_id 158 | INNER JOIN spark.record rec 159 | ON rec.entry_id = e.entry_id 160 | INNER JOIN spark.user u 161 | ON u.user_id = e.user_id 162 | WHERE e.is_out = FALSE 163 | AND e.is_deleted = FALSE 164 | AND e.is_kicked = FALSE 165 | AND u.is_deleted = FALSE 166 | AND u.push_remind = TRUE 167 | AND rec.date = $1 168 | AND e.room_id IN ( 169 | SELECT DISTINCT e.room_id FROM spark.entry e 170 | INNER JOIN spark.record r 171 | ON e.entry_id = r.entry_id 172 | WHERE r.date = $1 173 | AND r.status IN ('NONE', 'CONSIDER') 174 | AND e.is_kicked = FALSE 175 | AND e.is_deleted = FALSE 176 | AND e.is_kicked = FALSE 177 | ) 178 | `, 179 | [date], 180 | ); 181 | return convertSnakeToCamel.keysToCamel(rows); 182 | }; 183 | 184 | module.exports = { 185 | getRecordById, 186 | getRecentRecordByEntryId, 187 | updateStatusByRecordId, 188 | uploadRecord, 189 | getPagedRecordsByEntryId, 190 | getDonePagedRecordsByEntryId, 191 | insertRecords, 192 | getNoneOrConsiderEntryIdsByDate, 193 | getPushRemindUsers, 194 | }; 195 | -------------------------------------------------------------------------------- /functions/db/remind.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const insertRemind = async (client) => { 6 | const now = dayjs().add(9, 'hour'); 7 | const today = dayjs(now).format('YYYY-MM-DD'); 8 | const { rows } = await client.query( 9 | ` 10 | INSERT INTO spark.remind 11 | (date) 12 | VALUES 13 | ($1) 14 | RETURNING * 15 | `, 16 | [today], 17 | ); 18 | 19 | return convertSnakeToCamel.keysToCamel(rows); 20 | }; 21 | 22 | module.exports = { 23 | insertRemind, 24 | }; 25 | -------------------------------------------------------------------------------- /functions/db/report.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const checkAlreadyReport = async (client, userId, recordId) => { 6 | const { rows } = await client.query( 7 | ` 8 | SELECT * 9 | FROM spark.report 10 | WHERE user_id = $1 11 | AND record_id = $2 12 | `, 13 | [userId, recordId], 14 | ); 15 | 16 | return convertSnakeToCamel.keysToCamel(rows[0]); 17 | } 18 | 19 | const addReport = async (client, userId, targetUserId, recordId) => { 20 | const { rows } = await client.query( 21 | ` 22 | INSERT INTO spark.report 23 | (user_id, target_user_id, record_id) 24 | VALUES 25 | ($1, $2, $3) 26 | RETURNING * 27 | `, 28 | [userId, targetUserId, recordId], 29 | ); 30 | 31 | return convertSnakeToCamel.keysToCamel(rows); 32 | } 33 | 34 | module.exports = { 35 | addReport, 36 | checkAlreadyReport, 37 | }; 38 | -------------------------------------------------------------------------------- /functions/db/schedule.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | 6 | const insertSchedule = async (client) => { 7 | const now = dayjs().add(9, 'hour'); 8 | const today = dayjs(now).format('YYYY-MM-DD'); 9 | const { rows } = await client.query( 10 | ` 11 | INSERT INTO spark.schedule 12 | (date) 13 | VALUES 14 | ($1) 15 | RETURNING * 16 | `, 17 | [today] 18 | ); 19 | 20 | return convertSnakeToCamel.keysToCamel(rows); 21 | }; 22 | 23 | 24 | module.exports = { 25 | insertSchedule, 26 | }; 27 | -------------------------------------------------------------------------------- /functions/db/spark.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 3 | 4 | const countSparkByRecordId = async (client, recordId) => { 5 | const { rows } = await client.query( 6 | ` 7 | SELECT COUNT(*) 8 | FROM spark.spark 9 | WHERE record_id = $1 10 | `, 11 | [recordId], 12 | ); 13 | return convertSnakeToCamel.keysToCamel(rows[0]); 14 | }; 15 | 16 | const insertSpark = async (client, recordId, senderId, content) => { 17 | const { rows } = await client.query( 18 | ` 19 | INSERT INTO spark.spark 20 | (record_id, sender_id, content) 21 | VALUES 22 | ($1, $2, $3) 23 | RETURNING * 24 | `, 25 | [recordId, senderId, content], 26 | ); 27 | return convertSnakeToCamel.keysToCamel(rows[0]); 28 | } 29 | 30 | const countSparkByEntryId = async (client, entryId) => { 31 | const { rows } = await client.query( 32 | ` 33 | SELECT COUNT(*) 34 | FROM spark.spark s 35 | WHERE s.record_id IN ( 36 | SELECT r.record_id 37 | FROM spark.record r 38 | WHERE r.entry_id = $1 39 | ) 40 | `, 41 | [entryId], 42 | ); 43 | return convertSnakeToCamel.keysToCamel(rows); 44 | } 45 | 46 | const countSparkByRecordIds = async (client, recordIds) => { 47 | const { rows } = await client.query( 48 | ` 49 | SELECT s.record_id, COUNT(record_id) AS spark_num 50 | FROM spark.spark s 51 | WHERE record_id in (${recordIds.join()}) 52 | GROUP BY record_id 53 | `, 54 | ); 55 | return convertSnakeToCamel.keysToCamel(rows); 56 | } 57 | 58 | module.exports = { 59 | countSparkByRecordId, 60 | insertSpark, 61 | countSparkByEntryId, 62 | countSparkByRecordIds 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /functions/db/user.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const _ = require('lodash'); 3 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 4 | 5 | const getAllUsers = async (client) => { 6 | const { rows } = await client.query( 7 | ` 8 | SELECT * FROM spark.user as u 9 | WHERE is_deleted = FALSE 10 | `, 11 | ); 12 | return convertSnakeToCamel.keysToCamel(rows); 13 | }; 14 | 15 | const getUserById = async (client, userId) => { 16 | const { rows } = await client.query( 17 | ` 18 | SELECT * FROM spark.user as u 19 | WHERE user_id = $1 20 | AND is_deleted = FALSE 21 | `, 22 | [userId], 23 | ); 24 | return convertSnakeToCamel.keysToCamel(rows[0]); 25 | }; 26 | 27 | const getUserWithDelete = async (client, userId) => { 28 | const { rows } = await client.query( 29 | ` 30 | SELECT * FROM spark.user as u 31 | WHERE user_id = $1 32 | `, 33 | [userId], 34 | ); 35 | return convertSnakeToCamel.keysToCamel(rows[0]); 36 | }; 37 | 38 | const getUsersByIds = async (client, userIds) => { 39 | const { rows } = await client.query( 40 | ` 41 | SELECT DISTINCT * FROM spark.user 42 | WHERE user_id in (${userIds.join()}) 43 | AND is_deleted = FALSE 44 | `, 45 | ); 46 | return convertSnakeToCamel.keysToCamel(rows); 47 | }; 48 | 49 | const getUserBySocialId = async (client, socialId) => { 50 | const { rows } = await client.query( 51 | ` 52 | SELECT * FROM spark.user u 53 | WHERE social_id = $1 54 | AND is_deleted = FALSE 55 | `, 56 | [socialId], 57 | ); 58 | return convertSnakeToCamel.keysToCamel(rows[0]); 59 | }; 60 | 61 | const addUser = async (client, socialId, nickname, profileImg, fcmToken) => { 62 | const { rows } = await client.query( 63 | ` 64 | INSERT INTO spark.user 65 | (social_id, nickname, profile_img, device_token) 66 | VALUES 67 | ($1, $2, $3, $4) 68 | RETURNING user_id, nickname, profile_img 69 | `, 70 | [socialId, nickname, profileImg, fcmToken], 71 | ); 72 | return convertSnakeToCamel.keysToCamel(rows[0]); 73 | }; 74 | 75 | const updateDeviceTokenById = async (client, userId, fcmToken) => { 76 | const now = dayjs().add(9, 'hour'); 77 | const { rows } = await client.query( 78 | ` 79 | UPDATE spark.user 80 | SET device_token = $2, updated_at = $3 81 | WHERE user_id = $1 82 | RETURNING * 83 | `, 84 | [userId, fcmToken, now], 85 | ); 86 | return convertSnakeToCamel.keysToCamel(rows[0]); 87 | }; 88 | 89 | const updateDeviceTokenAndOsById = async (client, userId, fcmToken, os) => { 90 | const now = dayjs().add(9, 'hour'); 91 | const { rows } = await client.query( 92 | ` 93 | UPDATE spark.user 94 | SET device_token = $2, os = $3, updated_at = $4 95 | WHERE user_id = $1 96 | RETURNING * 97 | `, 98 | [userId, fcmToken, os, now], 99 | ); 100 | return convertSnakeToCamel.keysToCamel(rows[0]); 101 | }; 102 | 103 | const updateProfileById = async (client, userId, nickname, profileImg) => { 104 | const now = dayjs().add(9, 'hour'); 105 | const { rows } = await client.query( 106 | ` 107 | UPDATE spark.user 108 | SET nickname = $2, profile_img = $3, updated_at = $4 109 | WHERE user_id = $1 110 | RETURNING * 111 | `, 112 | [userId, nickname, profileImg, now], 113 | ); 114 | return convertSnakeToCamel.keysToCamel(rows[0]); 115 | }; 116 | 117 | const togglePushSettingById = async (client, userId, category) => { 118 | const categoryColumn = 'push_' + convertSnakeToCamel.keysToSnake([category])[0]; 119 | const now = dayjs().add(9, 'hour'); 120 | const { rows } = await client.query( 121 | ` 122 | UPDATE spark.user 123 | SET ${categoryColumn} = NOT ${categoryColumn}, updated_at = $2 124 | WHERE user_id = $1 125 | RETURNING * 126 | `, 127 | [userId, now], 128 | ); 129 | return convertSnakeToCamel.keysToCamel(rows[0]); 130 | }; 131 | 132 | const emptyDeviceTokenById = async (client, userId) => { 133 | const now = dayjs().add(9, 'hour'); 134 | const { rows } = await client.query( 135 | ` 136 | UPDATE spark.user 137 | SET device_token = '', updated_at = $2 138 | WHERE user_id = $1 139 | RETURNING * 140 | `, 141 | [userId, now], 142 | ); 143 | return convertSnakeToCamel.keysToCamel(rows[0]); 144 | }; 145 | 146 | const deleteUserSoft = async (client, userId, socialId) => { 147 | const now = dayjs().add(9, 'hour'); 148 | const nowToString = now.format('YYYYMMDDHHmmssSSS'); 149 | const newSocialId = `${socialId}-out-${nowToString}`; 150 | const { rows } = await client.query( 151 | ` 152 | UPDATE spark.user 153 | SET device_token = '', is_deleted = TRUE, social_id = $3, updated_at = $2, deleted_at = $2 154 | WHERE user_id = $1 155 | RETURNING * 156 | `, 157 | [userId, now, newSocialId], 158 | ); 159 | return convertSnakeToCamel.keysToCamel(rows[0]); 160 | }; 161 | 162 | module.exports = { 163 | getAllUsers, 164 | getUserById, 165 | getUsersByIds, 166 | getUserBySocialId, 167 | addUser, 168 | updateDeviceTokenById, 169 | updateDeviceTokenAndOsById, 170 | updateProfileById, 171 | togglePushSettingById, 172 | getUserWithDelete, 173 | emptyDeviceTokenById, 174 | deleteUserSoft, 175 | }; 176 | -------------------------------------------------------------------------------- /functions/db/version.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); 3 | 4 | const getRecentVersion = async (client) => { 5 | const { rows } = await client.query( 6 | ` 7 | SELECT * FROM spark.version 8 | ORDER BY created_at DESC 9 | LIMIT 1 10 | `, 11 | ); 12 | return convertSnakeToCamel.keysToCamel(rows[0]); 13 | }; 14 | 15 | const updateRecentVersion = async (client, newVersion) => { 16 | const now = dayjs().add(9, 'hour'); 17 | const { rows } = await client.query( 18 | ` 19 | INSERT INTO spark.version 20 | (version, created_at) 21 | VALUES 22 | ($1, $2) 23 | RETURNING * 24 | `, 25 | [newVersion, now], 26 | ); 27 | return convertSnakeToCamel.keysToCamel(rows[0]); 28 | }; 29 | 30 | module.exports = { 31 | getRecentVersion, 32 | updateRecentVersion, 33 | }; 34 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const serviceAccount = require('./we-sopt-spark-firebase-adminsdk-emnjd-30d5170309.json'); 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | let firebase; 8 | if (admin.apps.length === 0) { 9 | firebase = admin.initializeApp({ 10 | credential: admin.credential.cert(serviceAccount), 11 | }); 12 | } else { 13 | firebase = admin.app(); 14 | } 15 | 16 | module.exports = { 17 | api: require('./api'), 18 | }; 19 | -------------------------------------------------------------------------------- /functions/lib/convertSnakeToCamel.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const toCamel = (s) => { 4 | return s.replace(/([-_][a-z])/gi, ($1) => { 5 | return $1.toUpperCase().replace('-', '').replace('_', ''); 6 | }); 7 | }; 8 | 9 | const isArray = function (a) { 10 | return Array.isArray(a); 11 | }; 12 | 13 | const isObject = function (o) { 14 | return o === Object(o) && !isArray(o) && typeof o !== 'function'; 15 | }; 16 | 17 | const keysToCamel = function (o) { 18 | if (isObject(o)) { 19 | const n = {}; 20 | 21 | Object.keys(o).forEach((k) => { 22 | n[toCamel(k)] = o[k]; 23 | }); 24 | 25 | return n; 26 | } else if (isArray(o)) { 27 | return o.map((i) => { 28 | return keysToCamel(i); 29 | }); 30 | } 31 | 32 | return o; 33 | }; 34 | const keysToSnake = function (o) { 35 | if (isObject(o)) { 36 | const n = {}; 37 | 38 | Object.keys(o).forEach((k) => { 39 | n[_.snakeCase(k)] = o[k]; 40 | }); 41 | 42 | return n; 43 | } else if (isArray(o)) { 44 | return o.map((i) => { 45 | return _.snakeCase(i); 46 | }); 47 | } 48 | 49 | return o; 50 | }; 51 | 52 | module.exports = { 53 | keysToCamel, 54 | keysToSnake, 55 | }; 56 | -------------------------------------------------------------------------------- /functions/lib/deleteImage.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const { firebaseConfig } = require('../config/firebaseClient'); 3 | const { ownershipDB } = require('../db'); 4 | 5 | const deleteFirebaseImage = async (filePath) => { 6 | const fileName = filePath.split('.')[0]; 7 | const fileExtension = filePath.split('.')[1]; 8 | await admin.storage().bucket(firebaseConfig.storageBucket).file(`${fileName}.${fileExtension}`).delete(); 9 | await admin.storage().bucket(firebaseConfig.storageBucket).file(`${fileName}_270x270.${fileExtension}`).delete(); 10 | await admin.storage().bucket(firebaseConfig.storageBucket).file(`${fileName}_360x360.${fileExtension}`).delete(); 11 | await admin.storage().bucket(firebaseConfig.storageBucket).file(`${fileName}_720x720.${fileExtension}`).delete(); 12 | }; 13 | 14 | const deleteFirebaseImageByUserId = async (client, userId) => { 15 | const ownership = await ownershipDB.getOwnershipByUserId(client, userId); 16 | for (let i = 0; i < ownership.length; i++) { 17 | await deleteFirebaseImage(ownership[i].filePath); 18 | } 19 | await ownershipDB.deleteAllOwnershipByUserId(client, userId); 20 | }; 21 | 22 | module.exports = { 23 | deleteFirebaseImage, 24 | deleteFirebaseImageByUserId, 25 | }; 26 | -------------------------------------------------------------------------------- /functions/lib/jwtHandlers.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const jwt = require('jsonwebtoken'); 3 | const { TOKEN_INVALID, TOKEN_EXPIRED } = require('../constants/jwt'); 4 | const secretKey = process.env.JWT_SECRET; 5 | const options = { 6 | algorithm: 'HS256', 7 | expiresIn: '30d', 8 | issuer: 'spark', 9 | }; 10 | 11 | const sign = (user) => { 12 | const payload = { 13 | userId: user.userId 14 | }; 15 | 16 | const result = { 17 | accesstoken: jwt.sign(payload, secretKey, options), 18 | // refreshToken: jwt.sign(payload, secretKey, refreshOptions), 19 | }; 20 | return result; 21 | }; 22 | 23 | const verify = (token) => { 24 | let decoded; 25 | try { 26 | // console.log("token:",token); 27 | decoded = jwt.verify(token, secretKey); 28 | } catch (err) { 29 | if (err.message === 'jwt expired') { 30 | console.log('expired token'); 31 | return TOKEN_EXPIRED; 32 | } else if (err.message === 'invalid token') { 33 | console.log("decoded:", decoded); 34 | console.log('invalid token'); 35 | console.log(TOKEN_INVALID); 36 | return TOKEN_INVALID; 37 | } else { 38 | console.log('invalid token'); 39 | return TOKEN_INVALID; 40 | } 41 | } 42 | return decoded; 43 | }; 44 | 45 | module.exports = { 46 | sign, 47 | verify, 48 | }; -------------------------------------------------------------------------------- /functions/lib/passedDayToStr.js: -------------------------------------------------------------------------------- 1 | const passedDayToStr = (passedDay) => { 2 | if (passedDay === 0) { 3 | return '오늘'; 4 | } 5 | 6 | if (passedDay < 7) { 7 | return `${passedDay}일 전`; 8 | } 9 | 10 | if (passedDay < 30) { 11 | return `${Math.floor(passedDay / 7)}주 전`; 12 | } 13 | }; 14 | 15 | module.exports = { 16 | passedDayToStr, 17 | }; 18 | -------------------------------------------------------------------------------- /functions/lib/pushAlarm.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const functions = require('firebase-functions'); 3 | const util = require('./util'); 4 | const statusCode = require('../constants/statusCode'); 5 | const responseMessage = require('../constants/responseMessage'); 6 | 7 | const send = async (req, res, title, body, receiverToken, category, imageUrl = null, roomId = '', recordId = '') => { 8 | let mutableContent = 1; 9 | if (!imageUrl) { 10 | mutableContent = 0; 11 | imageUrl = ''; 12 | } 13 | 14 | if (!receiverToken) { 15 | return true; 16 | } 17 | 18 | if (!roomId) roomId = ''; 19 | if (!recordId) recordId = ''; 20 | 21 | try { 22 | const message = { 23 | data: { 24 | roomId: String(roomId), 25 | recordId: String(recordId), 26 | }, 27 | android: { 28 | data: { 29 | title, 30 | body, 31 | imageUrl, 32 | category, 33 | roomId: String(roomId), 34 | recordId: String(recordId), 35 | }, 36 | }, 37 | apns: { 38 | payload: { 39 | aps: { 40 | alert: { 41 | title, 42 | body, 43 | }, 44 | category, 45 | 'thread-id': category, 46 | 'mutable-content': mutableContent, 47 | }, 48 | }, 49 | fcm_options: { 50 | image: imageUrl, 51 | }, 52 | }, 53 | token: receiverToken, 54 | }; 55 | 56 | admin 57 | .messaging() 58 | .send(message) 59 | .then(function (response) { 60 | return true; 61 | }) 62 | .catch(function (err) { 63 | return res.status(400).json({ success: false }); 64 | }); 65 | return true; 66 | } catch (error) { 67 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 68 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 69 | } finally { 70 | } 71 | }; 72 | 73 | const getMessage = (title, body, receiverToken, category, imageUrl = null, roomId = '', recordId = '') => { 74 | let mutableContent = 1; 75 | if (!imageUrl) { 76 | mutableContent = 0; 77 | imageUrl = ''; 78 | } 79 | 80 | if (!roomId) roomId = ''; 81 | if (!recordId) recordId = ''; 82 | 83 | const message = { 84 | data: { 85 | roomId: String(roomId), 86 | recordId: String(recordId), 87 | }, 88 | android: { 89 | data: { 90 | title, 91 | body, 92 | imageUrl, 93 | category, 94 | roomId: String(roomId), 95 | recordId: String(recordId), 96 | }, 97 | }, 98 | apns: { 99 | payload: { 100 | aps: { 101 | alert: { 102 | title, 103 | body, 104 | }, 105 | category, 106 | 'thread-id': category, 107 | 'mutable-content': mutableContent, 108 | }, 109 | }, 110 | fcm_options: { 111 | image: imageUrl, 112 | }, 113 | }, 114 | token: receiverToken, 115 | }; 116 | return message; 117 | }; 118 | 119 | const sendMessages = async (req, res, messages) => { 120 | admin 121 | .messaging() 122 | .sendAll(messages) 123 | .then(function (response) { 124 | return true; 125 | }) 126 | .catch(function (err) { 127 | console.log(err); 128 | return false; 129 | }); 130 | }; 131 | 132 | const sendMulticastByTokens = async (req, res, title, body, receiverTokens, category, imageUrl = null, roomId = '', recordId = '') => { 133 | let mutableContent = 1; 134 | if (!imageUrl) { 135 | mutableContent = 0; 136 | imageUrl = ''; 137 | } 138 | 139 | if (!roomId) roomId = ''; 140 | if (!recordId) recordId = ''; 141 | 142 | // FCM Token이 empty인 경우 제외 143 | receiverTokens = receiverTokens.filter((t) => t); 144 | if (!receiverTokens.length) { 145 | return true; 146 | } 147 | 148 | try { 149 | const message = { 150 | data: { 151 | roomId: String(roomId), 152 | recordId: String(recordId), 153 | }, 154 | android: { 155 | data: { 156 | title, 157 | body, 158 | imageUrl, 159 | category, 160 | roomId: String(roomId), 161 | recordId: String(recordId), 162 | }, 163 | }, 164 | apns: { 165 | payload: { 166 | aps: { 167 | alert: { 168 | title, 169 | body, 170 | }, 171 | category, 172 | 'thread-id': category, 173 | 'mutable-content': mutableContent, 174 | }, 175 | }, 176 | fcm_options: { 177 | image: imageUrl, 178 | }, 179 | }, 180 | tokens: receiverTokens, 181 | }; 182 | 183 | admin 184 | .messaging() 185 | .sendMulticast(message) 186 | .then(function (response) { 187 | return true; 188 | }) 189 | .catch(function (err) { 190 | return res.status(400).json({ success: false }); 191 | }); 192 | return true; 193 | } catch (error) { 194 | functions.logger.error(`[ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`, `[CONTENT] ${error}`); 195 | return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 196 | } finally { 197 | } 198 | }; 199 | 200 | module.exports = { 201 | send, 202 | sendMulticastByTokens, 203 | getMessage, 204 | sendMessages, 205 | }; 206 | -------------------------------------------------------------------------------- /functions/lib/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | success: (status, message, data) => { 3 | return { 4 | status, 5 | success: true, 6 | message, 7 | data, 8 | }; 9 | }, 10 | fail: (status, message) => { 11 | return { 12 | status, 13 | success: false, 14 | message, 15 | }; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /functions/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const jwtHandlers = require('../lib/jwtHandlers'); 3 | const db = require('../db/db'); 4 | const util = require('../lib/util'); 5 | const statusCode = require('../constants/statusCode'); 6 | const responseMessage = require('../constants/responseMessage'); 7 | const slackAPI = require('./slackAPI'); 8 | const { userDB } = require('../db'); 9 | const { TOKEN_INVALID, TOKEN_EXPIRED } = require('../constants/jwt'); 10 | 11 | const checkUser = async (req, res, next) => { 12 | let client; 13 | try { 14 | client = await db.connect(req); 15 | 16 | let user; 17 | 18 | if (!req.headers) return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_AUTH_HEADER)); 19 | 20 | const token = String(req.headers.authorization || ''); 21 | if (!token) return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.TOKEN_EMPTY)); 22 | const decodedToken = jwtHandlers.verify(token); 23 | if (decodedToken === TOKEN_EXPIRED) return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.TOKEN_EXPIRED)); 24 | if (decodedToken === TOKEN_INVALID) return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.TOKEN_INVALID)); 25 | 26 | console.log(decodedToken); 27 | const userId = decodedToken.userId; 28 | 29 | if (!userId) return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.TOKEN_INVALID)); 30 | 31 | user = await userDB.getUserWithDelete(client, userId); 32 | 33 | if (!user) return res.status(statusCode.UNAUTHORIZED).send(util.fail(statusCode.UNAUTHORIZED, responseMessage.NO_USER)); 34 | if (user.isDeleted) return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.ALREADY_DELETED_USER)); 35 | req.user = user; 36 | } catch (error) { 37 | console.log(error); 38 | 39 | const slackMessage = `[ERROR BY ${req.user.nickname} (${req.user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${error} ${JSON.stringify(error)}`; 40 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 41 | 42 | res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 43 | } finally { 44 | client.release(); 45 | } 46 | 47 | next(); 48 | }; 49 | 50 | module.exports = { checkUser }; 51 | -------------------------------------------------------------------------------- /functions/middlewares/slackAPI.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const axios = require('axios'); 3 | 4 | const dotenv = require('dotenv'); 5 | 6 | dotenv.config(); 7 | 8 | // 슬랙 Webhook에서 발급받은 endpoint를 .env 파일에서 끌어옴 9 | // endpoint 자체는 깃허브에 올라가면 안 되기 때문! 10 | const DEV_WEB_HOOK_ERROR_MONITORING = process.env.DEV_WEB_HOOK_ERROR_MONITORING; 11 | const DEV_WEB_HOOK_FEED_REPORT = process.env.DEV_WEB_HOOK_FEED_REPORT; 12 | 13 | const sendMessageToSlack = (message, apiEndPoint = DEV_WEB_HOOK_ERROR_MONITORING) => { 14 | // 슬랙 Webhook을 이용해 슬랙에 메시지를 보내는 코드 15 | try { 16 | axios 17 | .post(apiEndPoint, { text: message }) 18 | .then((response) => {}) 19 | .catch((e) => { 20 | throw e; 21 | }); 22 | } catch (e) { 23 | console.error(e); 24 | // 슬랙 Webhook 자체에서 에러가 났을 경우, 25 | // Firebase 콘솔에 에러를 찍는 코드 26 | functions.logger.error('[slackAPI 에러]', { error: e }); 27 | } 28 | }; 29 | const feedReporotToSlack = (message, apiEndPoint = DEV_WEB_HOOK_FEED_REPORT) => { 30 | // 슬랙 Webhook을 이용해 슬랙에 메시지를 보내는 코드 31 | try { 32 | axios 33 | .post(apiEndPoint, { text: message }) 34 | .then((response) => {}) 35 | .catch((e) => { 36 | throw e; 37 | }); 38 | } catch (e) { 39 | console.error(e); 40 | // 슬랙 Webhook 자체에서 에러가 났을 경우, 41 | // Firebase 콘솔에 에러를 찍는 코드 42 | functions.logger.error('[slackAPI 에러]', { error: e }); 43 | } 44 | }; 45 | 46 | // 이 파일에서 정의한 변수 / 함수를 export 해서, 다른 곳에서 사용할 수 있게 해주는 코드 47 | module.exports = { 48 | sendMessageToSlack, 49 | DEV_WEB_HOOK_ERROR_MONITORING, 50 | feedReporotToSlack, 51 | DEV_WEB_HOOK_FEED_REPORT, 52 | }; -------------------------------------------------------------------------------- /functions/middlewares/uploadImage.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const functions = require('firebase-functions'); 3 | const BusBoy = require('busboy'); 4 | const path = require('path'); 5 | const os = require('os'); 6 | const fs = require('fs'); 7 | const dayjs = require('dayjs'); 8 | const db = require('../db/db'); 9 | const { ownershipDB } = require('../db'); 10 | const { firebaseConfig } = require('../config/firebaseClient'); 11 | const util = require('../lib/util'); 12 | const statusCode = require('../constants/statusCode'); 13 | const responseMessage = require('../constants/responseMessage'); 14 | const slackAPI = require('./slackAPI'); 15 | 16 | const uploadImageIntoSubDir = (subDir) => { 17 | return function (req, res, next) { 18 | const busboy = new BusBoy({ headers: req.headers }); 19 | 20 | let imageFileName = {}; 21 | let imagesToUpload = []; 22 | let imageToAdd = {}; 23 | let imageUrls = []; 24 | let filePaths = []; 25 | 26 | let fields = {}; 27 | 28 | // req.body로 들어오는 key:value 페어들을 처리 29 | busboy.on('field', (fieldName, val) => { 30 | fields[fieldName] = val; 31 | }); 32 | 33 | // req.body로 들어오는 게 파일일 경우 처리 34 | busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { 35 | if (mimetype !== 'image/jpeg' && mimetype !== 'image/png') { 36 | const slackMessage = `[Image Upload File Type Exception]\n [${req.user.nickname} (${req.user.userId})] tried to upload ${mimetype} file`; 37 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 38 | 39 | return res.status(400).json({ error: 'Wrong file type submitted' }); 40 | } 41 | // my.image.png => ['my', 'image', 'png'] 42 | const imageExtension = filename.split('.')[filename.split('.').length - 1]; 43 | // 32756238461724837.png 44 | imageFileName = `${dayjs().format('YYYYMMDD_HHmmss_')}${Math.round(Math.random() * 1000000000000).toString()}.${imageExtension}`; 45 | const filepath = path.join(os.tmpdir(), imageFileName); 46 | imageToAdd = { imageFileName, filepath, mimetype }; 47 | file.pipe(fs.createWriteStream(filepath)); 48 | imagesToUpload.push(imageToAdd); 49 | }); 50 | 51 | // req.body로 들어온 파일들을 Firebase Storage에 업로드 52 | busboy.on('finish', async () => { 53 | let promises = []; 54 | imagesToUpload.forEach((imageToBeUploaded) => { 55 | imageUrls.push(`https://firebasestorage.googleapis.com/v0/b/${firebaseConfig.storageBucket}/o/${subDir}%2F${imageToBeUploaded.imageFileName}?alt=media`); 56 | filePaths.push(`${subDir}/${imageToBeUploaded.imageFileName}`); 57 | promises.push( 58 | admin 59 | .storage() 60 | .bucket(firebaseConfig.storageBucket) 61 | .upload(imageToBeUploaded.filepath, { 62 | destination: `${subDir}/${imageToBeUploaded.filepath.split('/').slice(-1)[0]}`, 63 | resumable: false, 64 | metadata: { 65 | metadata: { 66 | contentType: imageToBeUploaded.mimetype, 67 | }, 68 | }, 69 | }), 70 | ); 71 | }); 72 | 73 | let client; 74 | try { 75 | // File에 대한 Ownership 등록 76 | client = await db.connect(req); 77 | if (req.user) { 78 | filePaths.forEach(async (filePath) => { 79 | await ownershipDB.insertOwnership(client, req.user.userId, filePath); 80 | }); 81 | } 82 | 83 | await Promise.all(promises); 84 | req.body = fields; 85 | req.imageUrls = imageUrls; 86 | next(); 87 | } catch (err) { 88 | functions.logger.error(`[FILE UPLOAD ERROR] [${req.method.toUpperCase()}] ${req.originalUrl}`); 89 | 90 | const slackMessage = `[ERROR BY ${req.user.nickname} (${req.user.userId})] [${req.method.toUpperCase()}] ${req.originalUrl} ${err} ${JSON.stringify(err)}`; 91 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 92 | return res.status(500).json(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.INTERNAL_SERVER_ERROR)); 93 | } finally { 94 | client.release(); 95 | } 96 | }); 97 | 98 | busboy.end(req.rawBody); 99 | }; 100 | }; 101 | module.exports = uploadImageIntoSubDir; 102 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "develop": "cross-env NODE_ENV=development firebase emulators:start --only functions", 7 | "serve": "cross-env NODE_ENV=production firebase emulators:start --only functions", 8 | "shell": "firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "cross-env NODE_ENV=production firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": "12" 15 | }, 16 | "main": "index.js", 17 | "dependencies": { 18 | "axios": "^0.24.0", 19 | "busboy": "^0.3.1", 20 | "cookie-parser": "^1.4.6", 21 | "cors": "^2.8.5", 22 | "cross-env": "^7.0.3", 23 | "dayjs": "^1.10.7", 24 | "dotenv": "^10.0.0", 25 | "eslint-config-prettier": "^8.3.0", 26 | "express": "^4.17.2", 27 | "firebase": "^9.5.0", 28 | "firebase-admin": "^10.3.0", 29 | "firebase-functions": "^3.21.2", 30 | "helmet": "^4.6.0", 31 | "hpp": "^0.2.3", 32 | "jsonwebtoken": "^8.5.1", 33 | "lodash": "^4.17.21", 34 | "multer": "^1.4.3", 35 | "nanoid": "^3.1.30", 36 | "node-schedule": "^2.1.0", 37 | "pg": "^8.7.1" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^7.6.0", 41 | "eslint-config-google": "^0.14.0", 42 | "firebase-functions-test": "^0.2.0" 43 | }, 44 | "private": true 45 | } 46 | -------------------------------------------------------------------------------- /functions/scheduler/funcs.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/db'); 2 | const { roomDB, recordDB, scheduleDB, remindDB, dialogDB, lifeTimelineDB } = require('../db'); 3 | const _ = require('lodash'); 4 | const dayjs = require('dayjs'); 5 | const slackAPI = require('../middlewares/slackAPI'); 6 | const alarmMessage = require('../constants/alarmMessage'); 7 | const pushAlarm = require('../lib/pushAlarm'); 8 | const { termList } = require('../constants/termList'); 9 | 10 | const checkLife = async () => { 11 | let client; 12 | try { 13 | client = await db.connect(); 14 | const scheduleCheck = await scheduleDB.insertSchedule(client); 15 | if (!scheduleCheck.length) { 16 | return; 17 | } 18 | 19 | const now = dayjs().add(9, 'hour'); 20 | const today = now.format('YYYY-MM-DD'); 21 | 22 | const ongoingRooms = await roomDB.getOngoingRoomIds(client); 23 | const ongoingRoomIds = ongoingRooms.map((o) => o.roomId); 24 | let failRecords = []; 25 | if (ongoingRoomIds.length) { 26 | failRecords = await roomDB.getFailRecords(client, ongoingRoomIds); // 습관방별 [실패한 record 개수(failCount)] 불러오기 27 | } 28 | const roomGroupByFailCount = _.groupBy(failRecords, 'failCount'); // failCount별 roomId 묶어주기 (ex. [{"failCount": 1, "roomId": [1,2,3]}, {"failCount":2, "roomId": [4,5,6]}]) 29 | const failCountList = [...new Set(failRecords.map((o) => Number(o.failCount)))]; // failCount 뭐뭐있는지~ (ex. [1,2,3]) 30 | const roomIdsByFailCount = { 1: [], 2: [], 3: [] }; 31 | failCountList.map((failCount) => { 32 | const roomIds = roomGroupByFailCount[failCount].map((o) => o.roomId); // 해당 failCount의 roomId배열 33 | if (failCount < 3) { 34 | roomIdsByFailCount[failCount] = roomIds; 35 | } else { 36 | // failCount 3보다 크면 3으로 일괄처리 37 | roomIdsByFailCount[3] = roomIdsByFailCount[3].concat(roomIds); 38 | } 39 | }); 40 | 41 | const lifeDeductionRooms = []; 42 | const lifeDeductionMap = new Map(); 43 | 44 | let afterLife = []; // 수명 깎아 준 후 습관방별 수명들 [{ roomId: 100, life: 1 }, ...] 45 | for (let i = 1; i <= 3; i++) { 46 | // 수명 깎아주기! - 3번 진행 (수명 1개깎이는 방 / 2개 깎이는 방 / 3개 깎이는 방) 47 | if (roomIdsByFailCount[i].length) { 48 | const updatedLife = await roomDB.updateLife(client, i, roomIdsByFailCount[i]); // { roomId: 100, life: 1 } 49 | updatedLife.map((o) => { 50 | if (o.life) { 51 | lifeDeductionRooms.push(o.roomId); 52 | lifeDeductionMap.set(o.roomId, i); 53 | } 54 | }); 55 | afterLife = afterLife.concat(updatedLife); 56 | const slackMessage = `[Life Deduction] life: -${i} / Target Room: ${roomIdsByFailCount[i]}`; 57 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 58 | } 59 | } 60 | 61 | const failRoomIds = _.filter(afterLife, { life: 0 }).map((o) => o.roomId); // 수명 깎아주고 나서 {life: 0} 이면 폭파된 방 62 | const successRoomIds = _.difference(ongoingRoomIds, failRoomIds); // 살아남은 방들 63 | let completeRooms = []; 64 | 65 | if (successRoomIds.length) { 66 | completeRooms = await roomDB.setRoomsComplete(client, successRoomIds); 67 | } 68 | const completeRoomIds = completeRooms.map((o) => o.roomId); 69 | const lifeDeductionRoomIds = _.difference(_.difference(lifeDeductionRooms, completeRoomIds), failRoomIds); 70 | const completeOrFailRoomIds = completeRoomIds.concat(failRoomIds); 71 | let dialogUsers = []; 72 | if (completeOrFailRoomIds.length) { 73 | dialogUsers = await roomDB.getAllUsersByIds(client, completeOrFailRoomIds); 74 | } 75 | let insertDialogs = []; 76 | dialogUsers.map((o) => { 77 | insertDialogs.push(`(${o.userId}, ${o.roomId}, '${o.status}', '${today}')`); 78 | }); 79 | if (insertDialogs.length) { 80 | await dialogDB.insertDialogs(client, insertDialogs); 81 | } 82 | 83 | let decreaseMessageUsers = []; 84 | if (lifeDeductionRoomIds.length) { 85 | decreaseMessageUsers = await roomDB.getAllUsersByIds(client, lifeDeductionRoomIds); 86 | } 87 | let failProfiles = {}; // 인증 안한 사용자 프로필 사진, key: roomId, value: profile 배열 88 | let decreaseCount = {}; // 인증 안한 사용자 수, key: roomId, value: decreaseCount 89 | let decreaseTimelines = []; 90 | for (let i = 0; i < decreaseMessageUsers.length; i++) { 91 | const { userId, roomId } = decreaseMessageUsers[i]; 92 | if (!Object.keys(failProfiles).includes(roomId)) { 93 | let profiles = await roomDB.getFailProfiles(client, roomId); 94 | profiles = profiles.map((p) => p.profile).sort(() => Math.random() - 0.5); 95 | decreaseCount[roomId] = profiles.length; 96 | while (profiles.length < 2) { 97 | profiles.push(null); 98 | } 99 | failProfiles[roomId] = profiles; 100 | } 101 | 102 | decreaseTimelines.push(`('${userId}', '${roomId}', true, ${decreaseCount[roomId]}, '${failProfiles[roomId][0]}', '${failProfiles[roomId][1]}')`); 103 | } 104 | 105 | // 생명 감소시 Time Line Insert 106 | if (decreaseTimelines.length) { 107 | await lifeTimelineDB.addDecreaseTimelines(client, decreaseTimelines); 108 | } 109 | 110 | // 살아남은 방 없으면 return 111 | if (!successRoomIds.length) { 112 | return; 113 | } 114 | 115 | const survivedRoomIds = _.difference(successRoomIds, completeRoomIds); 116 | const ongoingEntries = await roomDB.getEntriesByRoomIds(client, survivedRoomIds); // 성공한 방들의 entry 불러오기 117 | 118 | // 추가해줄 records 119 | let insertRecords = []; 120 | 121 | // 생명 충전 lifeTimelines 122 | let fillTimelines = []; 123 | 124 | // 생명 충전해줄 RoomIds 125 | let fillLifeRoomIds = new Set(); 126 | 127 | for (let i = 0; i < ongoingEntries.length; i++) { 128 | // insert할 record 값들 생성 129 | const entry = ongoingEntries[i]; 130 | const day = dayjs(today).diff(dayjs(entry.startAt), 'day') + 1; 131 | const record = '(' + entry.entryId + ",'" + now.format('YYYY-MM-DD') + "'," + day + ')'; 132 | insertRecords.push(record); 133 | 134 | if (termList.includes(day)) { 135 | fillLifeRoomIds.add(entry.roomId); 136 | fillTimelines.push(`('${entry.userId}', '${entry.roomId}', false, ${day})`); 137 | } 138 | } 139 | 140 | fillLifeRoomIds = Array.from(fillLifeRoomIds); 141 | 142 | if (insertRecords.length > 0) { 143 | await recordDB.insertRecords(client, insertRecords); // record 추가! 144 | } 145 | if (fillTimelines.length > 0) { 146 | await lifeTimelineDB.addFillTimelines(client, fillTimelines); // lifeTimeline 추가! 147 | } 148 | if (fillLifeRoomIds.length > 0) { 149 | await roomDB.fillLifeByRoomIds(client, fillLifeRoomIds); // 생명 충전 150 | // 생명 충전시, 습관 방의 분기가 변한 것이므로, entry 테이블의 term_new 값을 True로 업데이트 151 | await roomDB.updateTermNewByRoomIds(client, fillLifeRoomIds); 152 | } 153 | 154 | const slackMessage = `폭파된 방 목록: ${failRoomIds} \n 생명 충전 방 목록: ${fillLifeRoomIds} \n 살아남은 방 목록: ${survivedRoomIds}`; 155 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 156 | } catch (error) { 157 | console.log(error); 158 | const slackMessage = `[ERROR] ${error} ${JSON.stringify(error)}`; 159 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 160 | } finally { 161 | client.release(); 162 | } 163 | }; 164 | 165 | const sendRemind = async () => { 166 | let client; 167 | try { 168 | client = await db.connect(); 169 | 170 | const scheduleCheck = await remindDB.insertRemind(client); 171 | if (!scheduleCheck.length) { 172 | return; 173 | } 174 | 175 | const now = dayjs().add(9, 'hour'); 176 | const today = now.format('YYYY-MM-DD'); 177 | 178 | const remindUsers = await recordDB.getPushRemindUsers(client, today); 179 | const slackInfo = []; 180 | 181 | if (remindUsers.length) { 182 | const messages = []; 183 | 184 | remindUsers.map((u) => { 185 | if (u.status == 'NONE' || u.status == 'CONSIDER') { 186 | const { title, body, category } = alarmMessage.REMIND_ALERT_NONE(u.roomName); 187 | messages.push(pushAlarm.getMessage(title, body, u.deviceToken, category, null, u.roomId)); 188 | slackInfo.push(`[X]${u.userId}(${u.roomId})`); 189 | } else { 190 | const { title, body, category } = alarmMessage.REMIND_ALERT_DONE(u.roomName); 191 | messages.push(pushAlarm.getMessage(title, body, u.deviceToken, category, null, u.roomId)); 192 | slackInfo.push(`[O]${u.userId}(${u.roomId})`); 193 | } 194 | }); 195 | 196 | pushAlarm.sendMessages(null, null, messages); 197 | 198 | const slackMessage = `[REMIND SEND SUCCESS]: To ${slackInfo.length} users \n${slackInfo}`; 199 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 200 | return; 201 | } 202 | 203 | slackAPI.sendMessageToSlack('[REMIND NOT SENT]: 모든 방 습관인증 완료', slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 204 | return; 205 | } catch (error) { 206 | const slackMessage = `[ERROR] ${error} ${JSON.stringify(error)}`; 207 | slackAPI.sendMessageToSlack(slackMessage, slackAPI.DEV_WEB_HOOK_ERROR_MONITORING); 208 | } finally { 209 | client.release(); 210 | } 211 | }; 212 | 213 | module.exports = { 214 | checkLife, 215 | sendRemind, 216 | }; 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "dayjs": "^1.10.7" 4 | } 5 | } 6 | --------------------------------------------------------------------------------