├── .dockerignore ├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy-main.yml │ └── deploy-test.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── package.json ├── src ├── app.js ├── config.js ├── config │ └── winston.js ├── controller │ ├── auth.controller.js │ ├── board.controller.js │ ├── rotation.controller.js │ ├── slack.controller.js │ ├── timeline.controller.js │ ├── together.controller.js │ └── user.controller.js ├── data │ ├── auth.js │ ├── board.js │ ├── rotation.js │ ├── timeline.js │ ├── together.js │ └── user.js ├── db │ └── database.js ├── middleware │ ├── auth.js │ ├── rate-limiter.js │ ├── uploads.js │ └── validator.js ├── routes │ ├── auth.routes.js │ ├── board.routes.js │ ├── index.js │ ├── rotation.routes.js │ ├── slack.routes.js │ ├── timeline.routes.js │ ├── together.routes.js │ └── user.routes.js ├── s3.js ├── swagger │ ├── swagger.js │ └── swagger.sh └── utils │ ├── date.js │ ├── rotation.calendar.js │ ├── rotation.together.js │ └── slack.service.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es2021: true 4 | extends: 5 | - eslint:recommended 6 | - plugin:@typescript-eslint/recommended 7 | parser: "@typescript-eslint/parser" 8 | parserOptions: 9 | ecmaVersion: latest 10 | sourceType: module 11 | plugins: 12 | - "@typescript-eslint" 13 | rules: 14 | indent: 15 | - error 16 | - 2 17 | linebreak-style: 18 | - error 19 | - unix 20 | quotes: 21 | - error 22 | - double 23 | semi: 24 | - error 25 | comma-dangle: 26 | - error 27 | - always-multiline 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 투게더42 이슈 템플릿 3 | about: 투게더42 이슈 템플릿입니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## description 11 | > 이슈의 목적을 간결하게 서술해주세요 12 | ## todo 13 | - [ ] todo 1 14 | - [ ] todo 2 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy-main.yml: -------------------------------------------------------------------------------- 1 | name: main-deploy 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the main branch 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | name: Docker build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Set up Node 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: "18" 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | - name: build and release to DockerHub 25 | env: # 환경변수로 값을 지정하여 사용할 수 있습니다. 26 | REPO: kth2624 27 | LAYER_NAME: together 28 | run: | 29 | docker build -t $LAYER_NAME . 30 | docker tag $LAYER_NAME:latest $REPO/$LAYER_NAME:latest 31 | docker push $REPO/$LAYER_NAME:latest 32 | SSH: 33 | needs: build 34 | name: deploy 35 | runs-on: ubuntu-latest 36 | environment: together42 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Run scripts in server 41 | uses: appleboy/ssh-action@master 42 | with: 43 | host: ${{ secrets.KYU_HOST }} 44 | username: ubuntu 45 | key: ${{ secrets.AWS_KYU_KEY }} 46 | port: 22 47 | script: | 48 | mkdir -p backend 49 | echo "DB_DATABASE=${{ secrets.TEST_DATABASE }} 50 | DB_USER=${{ secrets.KYU_DB_USER }} 51 | DB_PASSWORD=${{ secrets.KYU_DB_PASSWORD }} 52 | DB_HOST=${{ secrets.KYU_DB_HOST }} 53 | DB_PORT=${{ secrets.KYU_DB_PORT }} 54 | JWT_SECRET=${{ secrets.JWT_SECRET }} 55 | JWT_EXPIRES_SEC=${{ secrets.JWT_EXPIRES_SEC }} 56 | BCRYPT_SALT_ROUNDS=${{ secrets.BCRYPT_SALT_ROUNDS }} 57 | HOST_PORT=${{ secrets.TEST_PORT }} 58 | NAVER_ID=${{ secrets.NAVER_ID }} 59 | NAVER_PW=${{ secrets.NAVER_PW }} 60 | ACCESS_KEY_ID=${{ secrets.ACCESS_KEY_ID }} 61 | SECRET_ACCESS_KEY=${{ secrets.SECRET_ACCESS_KEY }} 62 | BUCKET=${{ secrets.BUCKET }} 63 | REGION=${{ secrets.AWS_REGION }} 64 | DOCKER_IMAGE=${{ secrets.KYU_DOCKER_IMAGE }} 65 | BOT_USER_OAUTH_ACCESS_TOKEN=${{ secrets.BOT_USER_OAUTH_ACCESS_TOKEN }} 66 | SLACK_JIP=${{ secrets.SLACK_JIP }} 67 | SLACK_TKIM=${{ secrets.SLACK_TKIM }} 68 | SLACK_YWEE=${{ secrets.SLACK_YWEE }} 69 | OPENAPI_HOLIDAY_KEY=${{ secrets.OPENAPI_HOLIDAY_KEY }}" > ./backend/.env 70 | echo "start docker" 71 | docker stop backend || true 72 | docker rm backend || true 73 | docker rmi ${{ secrets.KYU_DOCKER_IMAGE }} || true 74 | cat < ./backend/docker-compose.yaml 75 | version: "3" 76 | services: 77 | backend: 78 | container_name: backend 79 | image: ${{ secrets.KYU_DOCKER_IMAGE }} 80 | restart: always 81 | volumes: 82 | - ./logs:/logs 83 | ports: 84 | - 3000:9999 85 | env_file: .env 86 | EOF 87 | cd backend && docker-compose up -d 88 | docker image prune -f 89 | -------------------------------------------------------------------------------- /.github/workflows/deploy-test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [test] 4 | 5 | jobs: 6 | build: 7 | name: Docker build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Set up Node 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: "18" 16 | - name: Login to DockerHub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: build and release to DockerHub 22 | env: # 환경변수로 값을 지정하여 사용할 수 있습니다. 23 | REPO: kth2624 24 | LAYER_NAME: together-test 25 | run: | 26 | docker build -t $LAYER_NAME . 27 | docker tag $LAYER_NAME:latest $REPO/$LAYER_NAME:latest 28 | docker push $REPO/$LAYER_NAME:latest 29 | SSH: 30 | needs: build 31 | name: deploy 32 | runs-on: ubuntu-latest 33 | environment: together42 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Run scripts in server 38 | uses: appleboy/ssh-action@master 39 | with: 40 | host: ${{ secrets.KYU_HOST }} 41 | username: ubuntu 42 | key: ${{ secrets.AWS_KYU_KEY }} 43 | port: 22 44 | script: | 45 | mkdir -p test_server 46 | echo "DB_DATABASE=${{ secrets.TEST_DATABASE }} 47 | DB_USER=${{ secrets.TEST_DATABASE_USER }} 48 | DB_PASSWORD=${{ secrets.TEST_DATABASE_PASSWORD }} 49 | DB_HOST=${{ secrets.TEST_DB_HOST }} 50 | DB_PORT=${{ secrets.TEST_DB_PORT }} 51 | JWT_SECRET=${{ secrets.JWT_SECRET }} 52 | JWT_EXPIRES_SEC=${{ secrets.JWT_EXPIRES_SEC }} 53 | BCRYPT_SALT_ROUNDS=${{ secrets.BCRYPT_SALT_ROUNDS }} 54 | HOST_PORT=${{ secrets.TEST_PORT }} 55 | NAVER_ID=${{ secrets.NAVER_ID }} 56 | NAVER_PW=${{ secrets.NAVER_PW }} 57 | ACCESS_KEY_ID=${{ secrets.ACCESS_KEY_ID }} 58 | SECRET_ACCESS_KEY=${{ secrets.SECRET_ACCESS_KEY }} 59 | BUCKET=${{ secrets.BUCKET }} 60 | REGION=${{ secrets.AWS_REGION }} 61 | DOCKER_IMAGE=${{ secrets.DOCKER_IMAGE }} 62 | BOT_USER_OAUTH_ACCESS_TOKEN=${{ secrets.BOT_TEST_USER_OAUTH_ACCESS_TOKEN }} 63 | SWAGGER_USER=${{ secrets.SWAGGER_USER }} 64 | SWAGGER_PASSWORD=${{ secrets.SWAGGER_PASSWORD }} 65 | SLACK_JIP=${{ secrets.SLACK_TEST_JIP }} 66 | SLACK_TKIM=${{ secrets.SLACK_TKIM }} 67 | SLACK_YWEE=${{ secrets.SLACK_YWEE }} 68 | OPENAPI_HOLIDAY_KEY=${{ secrets.OPENAPI_HOLIDAY_KEY }} 69 | BACKEND_TEST_HOST=dev.together.42jip.net" > ./test_server/.env 70 | echo "start docker" 71 | docker stop backend_test || true 72 | docker rm backend_test || true 73 | docker rmi ${{ secrets.DOCKER_IMAGE }} || true 74 | cat < ./test_server/docker-compose.yaml 75 | version: "3" 76 | services: 77 | backend: 78 | container_name: backend_test 79 | image: ${{ secrets.DOCKER_IMAGE }} 80 | restart: always 81 | volumes: 82 | - ./logs:/logs 83 | ports: 84 | - 9999:9999 85 | env_file: .env 86 | networks: 87 | - test_backend 88 | networks: 89 | test_backend: 90 | external: true 91 | EOF 92 | cd test_server && docker-compose up -d 93 | docker image prune -f 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/swagger/swagger-docs.json 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | .DS_Store 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | uploads/ 109 | .vscode 110 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | ADD . /app/ 6 | 7 | RUN rm yarn.lock || true 8 | RUN rm package-lock.json || true 9 | RUN yarn 10 | RUN yarn install 11 | RUN yarn swagger || true 12 | 13 | ENV HOST 0.0.0.0 14 | EXPOSE 9999 15 | 16 | CMD ["yarn", "prod"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 친해지길 바라! 2 | 3 |

4 | 5 |

6 | 7 |

8 | 🖥 For Client 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 |

23 | 🔐 For Server 24 |

25 | 26 |

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

35 | 36 | ## 🏠 [HOME PAGE](https://together42.github.io/frontend/) 37 | 38 | 집현전 사서들이 조금 더 많은 상호작용이 있으면 좋겠다는 생각을 했습니다. 더 많이 만나서 친해지기도 하고, 더 많은 프로젝트를 같이 할 수 있으면 좋을 것 같았습니다. 따라서 **신청자끼리 랜덤매칭을 하여 모임을 가지는 웹 서비스**를 제작했습니다. 39 | 40 | ## 📌 서비스 소개 41 | 42 | ### 🙋 원하는 이벤트에 참여 43 | 44 | > - 이벤트 목록에서 자신이 원하는 이벤트에 자유롭게 참가할 수 있습니다. 45 | > - 많은 이벤트에 참여할 수 있으며, 참석 취소도 가능합니다. 46 | 47 | ### 🤝 직접 이벤트 생성 48 | 49 | > - 자신이 원하는 모임을 자유롭게 만들어 신청을 유도할 수 있습니다. 50 | > - 저녁을 같이 먹을 사람을 구하는 사적인 모임 모집! 51 | > - 프로젝트원을 구하는 공적인 모임까지 생성할 수 있습니다. 52 | 53 | ### 🔖 이벤트 후기 작성 54 | 55 | > - 자신이 참여한 모임의 사진들을 업로드 할 수 있습니다. 56 | > - 모임을 같이 한 사람들과 대화를 나눌 수 있는 소통공간을 만듭니다. 57 | 58 | ### 👾 개성 있는 프로필 사진 59 | 60 | > - 자신이 원하는 프로필 사진을 골라 사용합니다. 61 | > - 무려 28가지의, 다양한 성별과 나이대의 프로필을 제공합니다. 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "together42", 3 | "version": "1.0.0", 4 | "description": "서먹서먹한 사람들이 친해지기 위해 밥 한 끼 먹게 해주는 랜덤매칭 웹 서비스", 5 | "main": "src/app.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "sudo node src/app.js", 10 | "swagger": "bash src/swagger/swagger.sh", 11 | "prod": "nodemon src/app.js", 12 | "dev": "yarn swagger && nodemon src/app.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Together42/Together.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Together42/Together/issues" 22 | }, 23 | "homepage": "https://github.com/Together42/Together#readme", 24 | "dependencies": { 25 | "@slack/web-api": "^6.7.2", 26 | "aws-sdk": "^2.1144.0", 27 | "bcrypt": "^5.0.1", 28 | "cookie-parser": "^1.4.6", 29 | "cors": "^2.8.5", 30 | "dotenv": "^16.0.0", 31 | "ejs": "^3.1.6", 32 | "express": "^4.17.3", 33 | "express-async-errors": "^3.1.1", 34 | "express-basic-auth": "^1.2.1", 35 | "express-rate-limit": "^6.4.0", 36 | "express-session": "^1.17.2", 37 | "express-validator": "^6.14.0", 38 | "fs": "^0.0.1-security", 39 | "jsonwebtoken": "^9.0.0", 40 | "morgan": "^1.10.0", 41 | "multer": "^1.4.4", 42 | "multer-s3": "^2.10.0", 43 | "mysql2": "^2.3.3", 44 | "node-cron": "^3.0.2", 45 | "nodemailer": "^6.7.5", 46 | "nodemon": "^2.0.15", 47 | "request": "^2.88.2", 48 | "swagger-autogen": "^2.22.0", 49 | "urlencode": "^1.1.0", 50 | "winston": "^3.7.2", 51 | "winston-daily-rotate-file": "^4.6.1" 52 | }, 53 | "devDependencies": { 54 | "@typescript-eslint/eslint-plugin": "^5.30.6", 55 | "@typescript-eslint/parser": "^5.30.6", 56 | "eslint": "^8.20.0", 57 | "swagger-jsdoc": "^6.2.1", 58 | "swagger-ui-express": "^4.4.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | import cors from "cors"; 4 | import swaggerUi from "swagger-ui-express"; 5 | import cookieParser from "cookie-parser"; 6 | import router from "./routes/index.js"; 7 | import swaggerFile from "./swagger/swagger-docs.json" assert { type: "json" }; 8 | import { config } from "./config.js"; 9 | import { stream } from "./config/winston.js"; 10 | import rateLimit from "./middleware/rate-limiter.js"; 11 | import expressBasicAuth from "express-basic-auth"; 12 | import cron from "node-cron"; 13 | import { 14 | createWeeklyDinnerEvent, 15 | matchWeeklyDinnerEvent, 16 | } from "./controller/together.controller.js"; 17 | import { 18 | initParticipants, 19 | postRotationMessage, 20 | } from "./controller/rotation.controller.js"; 21 | import { storeHolidayInfo } from "./utils/rotation.calendar.js"; 22 | import { postSlackTomorrowLibrarians } from "./utils/slack.service.js"; 23 | 24 | // express configuration 25 | const app = express(); 26 | 27 | //parse JSON and url-encoded query 28 | app.use(express.urlencoded({ extended: false })); 29 | app.use(express.json()); 30 | app.use( 31 | cors({ 32 | origin: [ 33 | process.env.BACKEND_LOCAL_HOST 34 | ? `http://${process.env.BACKEND_LOCAL_HOST}` 35 | : null, 36 | "http://localhost:3050", 37 | "http://10.18.245.57:3050", 38 | "http://10.19.230.111:3050", 39 | "https://together42.github.io", 40 | "https://together.42jip.net", 41 | ], 42 | credentials: true, 43 | }), 44 | ); 45 | app.use(cookieParser()); 46 | app.use(morgan("combined", { stream })); 47 | app.use(rateLimit); 48 | 49 | if (process.env.BACKEND_LOCAL_HOST || process.env.BACKEND_TEST_HOST) { 50 | app.use( 51 | ["/swagger"], 52 | expressBasicAuth({ 53 | challenge: true, 54 | users: { 55 | [process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD, 56 | }, 57 | }), 58 | ); 59 | 60 | //Swagger 연결 61 | app.use( 62 | "/swagger", 63 | swaggerUi.serve, 64 | swaggerUi.setup(swaggerFile, { 65 | explorer: true, 66 | swaggerOptions: { 67 | persistAuthorization: true, 68 | }, 69 | }), 70 | ); 71 | } 72 | 73 | // 매 달 첫 날, 다음 달에 휴일이 있는지 확인하여, DB에 저장. 74 | cron.schedule("0 0 1 * *", function() { 75 | storeHolidayInfo() 76 | .then(response => { 77 | console.log(`storeHolidayInfo status: ${response.status}`); 78 | }) 79 | .catch(error => { 80 | console.log('Error occurred in storeHolidayInfo:'); 81 | console.log(error); 82 | }); 83 | }); 84 | 85 | // 매 요일 날짜 확인 후, 로테이션에 해당하는 날짜에 행동을 수행 86 | // 1. 로테이션 시작 당일 : initParticipants 87 | // 2. 로테이션 주간 중 수요일, 금요일 : postRotationMessage 88 | cron.schedule("0 12 * * *", function () { 89 | initParticipants() 90 | .then(response => { 91 | console.log(`initParticipants status: ${response.status}`); 92 | }) 93 | .catch(error => { 94 | console.log('Error occurred in initParticipants:'); 95 | console.log(error); 96 | }); 97 | postRotationMessage() 98 | .then(response => { 99 | console.log(`postRotationMessgae status: ${response.status}`); 100 | }) 101 | .catch(error => { 102 | console.log('Error occuured in PostRotationMessage: '); 103 | console.log(error); 104 | }); 105 | }); 106 | 107 | // 주간 회의 후 저녁 식사 친바 자동 생성 108 | cron.schedule("42 16 * * 3", createWeeklyDinnerEvent, { 109 | timezone: "Asia/Seoul", 110 | }); 111 | 112 | // 주간 회의 후 저녁 식사 친바 자동 매칭 113 | cron.schedule("0 18 * * 3", matchWeeklyDinnerEvent, { 114 | timezone: "Asia/Seoul", 115 | }); 116 | 117 | // 매일 오전 10시 사서에게 알림 118 | cron.schedule("0 10 * * *", postSlackTomorrowLibrarians, { 119 | timezone: "Asia/Seoul", 120 | }); 121 | 122 | //route 123 | app.use("/api", router); 124 | 125 | app.listen(config.host.port); 126 | console.log("Listening on", config.host.port); 127 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import nodemailer from "nodemailer"; 3 | 4 | dotenv.config(); 5 | 6 | function required(key, defaultValue = undefined) { 7 | const value = process.env[key] || defaultValue; 8 | if (value == null) { 9 | throw new Error(`Key ${key} is undefined`); 10 | } 11 | return value; 12 | } 13 | 14 | export const config = { 15 | jwt: { 16 | secretKey: required("JWT_SECRET"), 17 | expiresInSec: parseInt(required("JWT_EXPIRES_SEC", 86400)), 18 | }, 19 | bcrypt: { 20 | saltRounds: parseInt(required("BCRYPT_SALT_ROUNDS", 10)), 21 | }, 22 | host: { 23 | port: parseInt(required("HOST_PORT", 8080)), 24 | }, 25 | db: { 26 | host: required("DB_HOST"), 27 | user: required("DB_USER"), 28 | database: required("DB_DATABASE"), 29 | password: required("DB_PASSWORD"), 30 | port: required("DB_PORT"), 31 | }, 32 | hostname: { 33 | hostname: required("HOSTNAME", "local"), 34 | }, 35 | serverUrl: { 36 | serverUrl: required("SERVER_URL", "http://localhost:8080/"), 37 | }, 38 | s3: { 39 | access_key_id: required("ACCESS_KEY_ID"), 40 | secret_access_key: required("SECRET_ACCESS_KEY"), 41 | bucket: required("BUCKET"), 42 | region: required("REGION"), 43 | }, 44 | naver: { 45 | id: required("NAVER_ID"), 46 | pw: required("NAVER_PW"), 47 | }, 48 | slack: { 49 | jip: required("SLACK_JIP"), 50 | tkim: required("SLACK_TKIM"), 51 | ywee: required("SLACK_YWEE"), 52 | slack_token: required("BOT_USER_OAUTH_ACCESS_TOKEN"), 53 | }, 54 | rateLimit: { 55 | windowMs: 60 * 1000, //1분 56 | maxRequest: 100, //ip 1개당 100번 57 | }, 58 | openApi: { 59 | holidayKey: required("OPENAPI_HOLIDAY_KEY"), 60 | }, 61 | }; 62 | 63 | export const smtpTransport = nodemailer.createTransport({ 64 | service: "Naver", 65 | host: "smtp.naver.com", 66 | port: 587, 67 | auth: { 68 | user: config.naver.id, 69 | pass: config.naver.pw, 70 | }, 71 | tls: { 72 | rejectUnauthorized: false, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /src/config/winston.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import winston, { format } from "winston"; 4 | import winstonDaily from "winston-daily-rotate-file"; 5 | const { combine, timestamp, printf } = format; 6 | 7 | const customFormat = printf((info) => { 8 | return `${info.timestamp} ${info.level}: ${info.message}`; 9 | }); 10 | 11 | const logger = winston.createLogger({ 12 | format: combine( 13 | timestamp({ 14 | format: "YYYY-MM-DD HH:mm:ss", 15 | }), 16 | customFormat, 17 | ), 18 | transports: [ 19 | new winston.transports.Console(), 20 | 21 | new winstonDaily({ 22 | level: "info", 23 | datePattern: "YYYYMMDD", 24 | dirname: "./logs", 25 | filename: "together_%DATE%.log", 26 | maxSize: null, 27 | maxFiles: 14, 28 | }), 29 | ], 30 | }); 31 | 32 | const stream = { 33 | write: (message) => { 34 | logger.info(message); 35 | }, 36 | }; 37 | 38 | export { logger, stream }; 39 | -------------------------------------------------------------------------------- /src/controller/auth.controller.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import bcrypt from "bcrypt"; 3 | import {} from "express-async-errors"; 4 | import * as userRepository from "../data/auth.js"; 5 | import { config, smtpTransport } from "../config.js"; 6 | import urlencoded from "urlencode"; 7 | 8 | const generateRandom = function (min, max) { 9 | const ranNum = Math.floor(Math.random() * (max - min + 1)) + min; 10 | return ranNum.toString(); 11 | }; 12 | 13 | export async function cert(req, res) { 14 | const CEA = req.body.CEA; 15 | const hashNum = urlencoded.decode(req.cookies.hashNum); 16 | const compare = await bcrypt.compare(CEA, hashNum); 17 | 18 | try { 19 | if (compare) { 20 | res.status(200).send({ result: "success" }); 21 | } else { 22 | res.status(400).send({ result: "fail" }); 23 | } 24 | } catch (err) { 25 | res.status(400).send({ result: "fail" }); 26 | console.error(err); 27 | } 28 | } 29 | 30 | export async function mailAuthentication(req, res) { 31 | //이메일 보내기 32 | const { sendEmail } = req.body; 33 | console.log(sendEmail); 34 | 35 | let number = generateRandom(111111, 999999); 36 | const hashNum = await bcrypt.hash(number, config.bcrypt.saltRounds); 37 | console.log(hashNum); 38 | res.cookie("hashNum", hashNum.toString(), { 39 | maxAge: 300000, 40 | domain: ".together.42jip.net", 41 | }); 42 | 43 | const mailOptions = { 44 | from: config.naver.id, 45 | to: sendEmail, 46 | subject: "[친바]인증 관련 이메일 입니다.", 47 | text: "인증번호는 " + number + " 입니다.", 48 | html: 49 | "
" + 50 | "

" + 51 | "Together42
" + 52 | "인증번호 안내입니다." + 53 | "

" + 54 | "

" + 55 | "안녕하세요.
" + 56 | "요청하신 인증번호가 생성되었습니다.
" + 57 | "감사합니다." + 58 | "

" + 59 | "

" + 60 | "인증번호:
" + 61 | "" + 62 | number + 63 | "" + 64 | "

" + 65 | "
" + 66 | "
" + 67 | "
", 68 | }; 69 | await smtpTransport.sendMail(mailOptions, (err, res) => { 70 | // 메일을 보내는 코드 71 | if (err) { 72 | console.log(err); 73 | res.status(400).json({ data: "err" }); 74 | } 75 | smtpTransport.close(); 76 | }); 77 | res.status(200).send({ data: "success" }); 78 | } 79 | 80 | export async function signUp(req, res) { 81 | const { intraId, password, email, profile } = req.body; 82 | const user = await userRepository.findByintraId(intraId); 83 | console.log(user); 84 | if (user) { 85 | //이미 존재하는 사용자라면 86 | return res.status(400).json({ message: `${intraId}는 이미 사용중입니다` }); 87 | } 88 | 89 | const checkEmail = await userRepository.findByEmail(email); 90 | if (checkEmail) { 91 | //이미 존재하는 이메일 92 | return res.status(400).json({ message: `${email}는 이 사용중입니다` }); 93 | } 94 | const hashed = await bcrypt.hash(password, config.bcrypt.saltRounds); 95 | const userId = await userRepository.createUser({ 96 | intraId, 97 | password: hashed, 98 | email, 99 | profile, 100 | }); 101 | const token = createJwtToken(userId, 0); //가입 후 isAdmin 디폴트값은 0 102 | console.log(`signUp: ${intraId}, time : ${new Date()}`); 103 | res.status(201).json({ token, intraId }); 104 | } 105 | 106 | export async function login(req, res) { 107 | const { intraId, password } = req.body; 108 | const user = await userRepository.findByintraId(intraId); 109 | if (!user) { 110 | //사용자가 존재하는지 검사 111 | return res.status(401).json({ message: "아이디와 비밀번호가 틀렸습니다" }); 112 | } 113 | const isValidPassword = await bcrypt.compare(password, user.password); 114 | if (!isValidPassword) { 115 | //비밀먼호 검증 116 | return res.status(401).json({ message: "아이디와 비밀번호가 틀렸습니다" }); 117 | } 118 | const profile = user.profile; 119 | const token = createJwtToken(user.intraId, user.isAdmin); 120 | console.log(`login id : ${intraId}, time : ${new Date()}`); 121 | res.status(200).json({ token, intraId, profile }); 122 | } 123 | 124 | function createJwtToken(id, isAdmin) { 125 | return jwt.sign({ id, isAdmin }, config.jwt.secretKey, { 126 | expiresIn: config.jwt.expiresInSec, 127 | }); 128 | } 129 | 130 | export async function me(req, res) { 131 | const user = await userRepository.findById(req.userId); 132 | if (!user) { 133 | return res.status(404).json({ mesage: "사용자가 없습니다" }); 134 | } 135 | res.status(200).json({ token: req.token, intraId: user.intraId }); 136 | } 137 | 138 | export async function getByUserList(req, res) { 139 | const userList = await userRepository.getByUserList(); 140 | if (!userList) { 141 | return res.status(404).json({ mesage: "사용자가 없습니다" }); 142 | } 143 | res.status(200).json({ userList: userList }); 144 | } 145 | 146 | export async function getByUserInfo(req, res) { 147 | const intraId = req.params.id; 148 | const userInfo = await userRepository.findByintraId(intraId); 149 | if (!userInfo) return res.status(400).json({ message: "사용자가 없습니다." }); 150 | res.status(200).json(userInfo); 151 | } 152 | 153 | export async function updatePassword(req, res) { 154 | const id = req.params.id; 155 | const { intraId, password } = req.body; 156 | const hashed = await bcrypt.hash(password, config.bcrypt.saltRounds); 157 | const updated = await userRepository.updatePassword({ 158 | id: id, 159 | intraId: intraId, 160 | password: hashed, 161 | }); 162 | res.status(200).json({ updated }); 163 | } 164 | -------------------------------------------------------------------------------- /src/controller/board.controller.js: -------------------------------------------------------------------------------- 1 | import * as boardRepository from "../data/board.js"; 2 | import * as userRepository from "../data/user.js"; 3 | import { publishMessage } from "./slack.controller.js"; 4 | import { s3 } from "../s3.js"; 5 | import { config } from "../config.js"; 6 | 7 | //게시글 생성 8 | export async function createPost(req, res) { 9 | const { title, contents, eventId, attendMembers } = req.body; 10 | console.log(attendMembers); 11 | const writerId = req.userId; 12 | 13 | const check = await boardRepository.checkAttendMember(attendMembers); //참석유저검증 14 | if (check.length !== attendMembers.length) 15 | return res.status(400).json({ message: "없는 유저입니다" }); 16 | if (title == "") 17 | return res.status(400).json({ message: "제목을 넣어주세요" }); 18 | const post = await boardRepository.createPost({ 19 | writerId, 20 | title, 21 | contents, 22 | eventId, 23 | }); 24 | 25 | //check.map(async (member)=>{//슬랙봇 메시지 보내기 26 | // if(member.slackId) 27 | // await publishMessage(member.slackId, str) 28 | //}) 29 | console.log(post); 30 | 31 | await boardRepository.createAttendMember(attendMembers, post); 32 | res.status(201).json({ post }); 33 | } 34 | 35 | //게시글 삭제 36 | export async function deletePost(req, res) { 37 | const id = req.params.id; 38 | const deleteId = await boardRepository.findByPostId(id); 39 | console.log(deleteId); 40 | 41 | if (!deleteId) 42 | //삭제할 글이 없다면 43 | return res.status(404).json({ message: "삭제할 게시글이 없습니다" }); 44 | if (deleteId.writerId !== req.userId && !req.isAdmin) 45 | //권한 46 | return res.status(401).json({ message: "권한이 없습니다" }); 47 | const imageId = await boardRepository.getImages(id); 48 | console.log(imageId); 49 | imageId.map((image) => { 50 | deleteObjectOfS3(image.fileKey); 51 | }); 52 | await boardRepository.deletePost(id); 53 | res.sendStatus(204); 54 | } 55 | 56 | //게시글 수정 57 | //일단 제목, 내용만 수정가능, / 사진은 추후에 58 | export async function updatePost(req, res) { 59 | const id = req.params.id; 60 | const { title, contents, eventId, attendMembers } = req.body; 61 | //제목이 없을시 에러 62 | if (title == "") 63 | return res.status(400).json({ message: "제목을 넣어주세요" }); 64 | 65 | const updateId = await boardRepository.findByPostId(id); 66 | if (!updateId) { 67 | //해당 게시글이 없다면 68 | return res.status(404).json({ message: "게시글이 없습니다" }); 69 | } 70 | if (updateId.writerId !== req.userId && !req.isAdmin) { 71 | return res.status(401).json({ message: "권한이 없습니다" }); 72 | } 73 | const updated = await boardRepository.updatePost({ 74 | id, 75 | title, 76 | contents, 77 | eventId, 78 | attendMembers, 79 | }); 80 | res.status(200).json({ updated }); 81 | } 82 | 83 | export async function getBoardList(req, res) { 84 | const eventId = req.query.eventId; 85 | let boardList; 86 | console.log(`eventId = ${eventId}`); 87 | try { 88 | const list = await boardRepository.getBoardList(eventId); 89 | boardList = await Promise.all( 90 | list.map(async (board) => { 91 | const imageList = await boardRepository.getImages(board.boardId); 92 | board.images = imageList; 93 | return board; 94 | }), 95 | ); 96 | } catch (error) { 97 | return res.status(400).json({ message: "게시판 조회 실패" }); 98 | } 99 | res.status(200).json({ boardList }); 100 | } 101 | 102 | export async function getBoardDetail(req, res) { 103 | const boardId = req.params.id; 104 | const board = await boardRepository.getBoard(boardId); 105 | if (!board) return res.status(400).json({ message: "게시글이 없습니다" }); 106 | try { 107 | const attendMembers = await boardRepository.getAttendMembers(boardId); 108 | const comments = await boardRepository.getComments(boardId); 109 | const image = await boardRepository.getImages(boardId); 110 | board.images = image; 111 | board.attendMembers = attendMembers; 112 | board.comments = comments; 113 | } catch (error) { 114 | return res.status(400).json({ message: "상세조회 실패" }); 115 | } 116 | console.log(board); 117 | 118 | res.status(200).json(board); 119 | } 120 | 121 | //comment 122 | 123 | export async function createComment(req, res) { 124 | const { boardId, comment } = req.body; 125 | const writerId = req.userId; 126 | console.log( 127 | `boardId = ${boardId}, comment = ${comment}, writerId = ${writerId}`, 128 | ); 129 | const result = await boardRepository.createComment( 130 | boardId, 131 | comment, 132 | writerId, 133 | ); 134 | // 게시글에 댓글이 달리면 슬랙 메세지를 보낸다. 135 | const matchedPost = await boardRepository.findByPostId(boardId); 136 | const writerInfo = await userRepository.findUserById(matchedPost.writerId); 137 | if (writerInfo.slackId) { 138 | let str = `${matchedPost.title} 게시글에 댓글이 달렸습니다.\nhttps://together42.github.io/frontend/review`; 139 | await publishMessage(writerInfo.slackId, str); 140 | } else { 141 | console.log("board.controller.js : Slack 댓글 알림 메세지 보내기 실패."); 142 | } 143 | res.status(200).json({ result }); 144 | } 145 | 146 | export async function updateComment(req, res) { 147 | const id = req.params.id; 148 | const comment = req.body.comment; 149 | const writerId = req.userId; 150 | console.log(`comment = ${comment}, writerId = ${writerId}`); 151 | const commentId = await boardRepository.findByCommentId(id); 152 | if (writerId !== commentId.writerId) 153 | return res.status(401).json({ message: "권한이 없습니다" }); 154 | 155 | const comments = await boardRepository.updateComment(comment, id); 156 | console.log(comments); 157 | res.status(200).json({ comments }); 158 | } 159 | 160 | export async function deleteComment(req, res) { 161 | const id = req.params.id; 162 | const deleteId = await boardRepository.findByCommentId(id); 163 | console.log(deleteId); 164 | 165 | if (!deleteId) 166 | //삭제할 댓글이 없다면 167 | return res.status(404).json({ message: "삭제할 댓글이 없습니다" }); 168 | if (deleteId.writerId !== req.userId && !req.isAdmin) 169 | //권한 170 | return res.status(401).json({ message: "권한이 없습니다" }); 171 | 172 | await boardRepository.deleteComment(id); 173 | res.sendStatus(204); 174 | } 175 | 176 | //파일 업로드 177 | 178 | export async function upload(req, res) { 179 | const boardId = req.body.boardId; 180 | const image = req.files; 181 | console.log( 182 | `image length = ${image.length}, fileValidationError = ${req.fileValidationError}`, 183 | ); 184 | console.log(image[0]); 185 | const path = image.map((img) => img.location); 186 | if (req.fileValidationError) { 187 | //파일이 크거나 형식이 다를때 188 | return res.status(400).send({ message: req.fileValidationError }); 189 | } 190 | if (image.length < 1) { 191 | //이미지가 없을때 192 | return res.status(400).send(util.fail(400, "이미지가 없습니다")); 193 | } 194 | const imageId = await boardRepository.imageUpload(boardId, image); 195 | if (imageId.errno) 196 | return res.status(400).send({ message: "잘못된 boardId입니다" }); 197 | return res.status(200).send( 198 | util.success(200, "업로드를 완료했습니다", { 199 | imageId: imageId, 200 | path: path, 201 | }), 202 | ); 203 | } 204 | 205 | export async function deleteImage(req, res) { 206 | const id = req.params.id; 207 | const deleteId = await boardRepository.findByImageId(id); 208 | console.log(deleteId); 209 | 210 | if (!deleteId) 211 | //삭제할 이미지가 없다면 212 | return res.status(404).json({ message: "삭제할 사진이 없습니다" }); 213 | try { 214 | deleteObjectOfS3(deleteId.fileKey); //s3에서 이미지 삭제 215 | await boardRepository.deleteImage(id); //db에서 지우는 작업 216 | res.sendStatus(204); 217 | } catch (e) { 218 | console.log(e); 219 | } 220 | } 221 | 222 | function deleteObjectOfS3(fileKey) { 223 | s3.deleteObject( 224 | { 225 | //s3에서 삭제 226 | Bucket: config.s3.bucket, 227 | Key: fileKey, 228 | }, 229 | function (err, data) { 230 | if (err) console.log(err); 231 | console.log(data); 232 | }, 233 | ); 234 | } 235 | 236 | const util = { 237 | success: (status, message, data) => { 238 | return { 239 | status: status, 240 | success: true, 241 | message: message, 242 | data: data, 243 | }; 244 | }, 245 | fail: (status, message) => { 246 | return { 247 | status: status, 248 | success: false, 249 | message: message, 250 | }; 251 | }, 252 | }; 253 | -------------------------------------------------------------------------------- /src/controller/rotation.controller.js: -------------------------------------------------------------------------------- 1 | import * as rotationRepository from "../data/rotation.js"; 2 | import * as rotationUtils from "../utils/rotation.together.js"; 3 | import { 4 | getTodayDate, 5 | getFourthWeekdaysOfMonth, 6 | } from "../utils/rotation.calendar.js"; 7 | import { publishMessage } from "./slack.controller.js"; 8 | import { config } from "../config.js"; 9 | 10 | export async function initParticipants() { 11 | const results = await rotationRepository.getAttendableUsers(); 12 | const users = results.map(item => item.intraId); 13 | const month = new Date().getMonth() + 2; 14 | const year = month === 1 ? new Date().getFullYear() += 1 : new Date().getFullYear(); 15 | const today = getTodayDate(); 16 | if (getFourthWeekdaysOfMonth()[0] != today) { 17 | return { status: 200 } 18 | } 19 | return new Promise((resolve, reject) => { 20 | try { 21 | const promises = users.map(async (user) => { 22 | let participant = []; 23 | participant['intraId'] = user; 24 | participant['attendLimit'] = []; 25 | participant['month'] = month; 26 | participant['year'] = year; 27 | await rotationRepository.addParticipant(participant); 28 | }); 29 | 30 | Promise.all(promises) 31 | .then(async () => { 32 | const rotationResult = await setRotation(); 33 | if (rotationResult.status < 0) 34 | reject ({ status: 500, message: 'setRotation failed in initParticipants'}); 35 | else 36 | resolve ({ status: 200, message: 'initParticipants success '}); 37 | }) 38 | } catch (error) { 39 | reject ({ status: 500, message: 'initParticipants failed', error: error }); 40 | } 41 | }) 42 | } 43 | 44 | export async function addParticipant(req, res) { 45 | let participant = req.body; 46 | let year = new Date().getFullYear(); 47 | let month = new Date().getMonth(); 48 | let nextMonth = ((month + 1) % 12) + 1; 49 | if (nextMonth === 1) year += 1; 50 | try { 51 | if (getFourthWeekdaysOfMonth().indexOf(getTodayDate()) < 0) { 52 | return res 53 | .status(500) 54 | .json({ message: "사서 로테이션 기간이 아닙니다." }); 55 | } 56 | const exists = await rotationRepository.getParticipantInfo({ 57 | intraId: participant.intraId, 58 | month: nextMonth, 59 | year: year, 60 | }); 61 | if (!exists.length) { 62 | participant["month"] = nextMonth; 63 | participant["year"] = year; 64 | if (participant.attendLimit === null) participant["attendLimit"] = []; 65 | await rotationRepository.addParticipant(participant); 66 | let rotationResult = setRotation(); 67 | if (rotationResult.status < 0) { 68 | return res 69 | .status(500) 70 | .json({ message: "사서 로테이션을 실패하였습니다." }); 71 | } 72 | return res.status(200).json({ 73 | intraId: participant.intraId, 74 | message: "로테이션 참석이 완료되었습니다.", 75 | }); 76 | } else { 77 | return res.status(400).json({ 78 | intraId: participant.intraId, 79 | message: "중복되는 참석자입니다.", 80 | }); 81 | } 82 | } catch (error) { 83 | console.log(error); 84 | return res.status(500).json({ message: "참석자 추가 실패." }); 85 | } 86 | } 87 | 88 | export async function deleteParticipant(req, res) { 89 | let participant = req.body; 90 | let year = new Date().getFullYear(); 91 | let month = new Date().getMonth(); 92 | let nextMonth = ((month + 1) % 12) + 1; 93 | if (nextMonth === 1) year += 1; 94 | try { 95 | if (getFourthWeekdaysOfMonth().indexOf(getTodayDate()) < 0) { 96 | return res 97 | .status(500) 98 | .json({ message: "사서 로테이션 기간이 아닙니다." }); 99 | } 100 | const exists = await rotationRepository.getParticipantInfo({ 101 | intraId: participant.intraId, 102 | month: nextMonth, 103 | year: year, 104 | }); 105 | if (exists.length) { 106 | await rotationRepository.deleteParticipant({ 107 | intraId: participant.intraId, 108 | month: nextMonth, 109 | year: year, 110 | }); 111 | let rotationResult = setRotation(); 112 | if (rotationResult.status < 0) { 113 | return res 114 | .status(500) 115 | .json({ message: "사서 로테이션을 실패하였습니다." }); 116 | } 117 | return res.status(200).json({ 118 | intraId: participant.intraId, 119 | message: "로테이션 참석 정보를 삭제했습니다.", 120 | }); 121 | } else { 122 | return res.status(400).json({ 123 | intraId: participant.intraId, 124 | message: "다음 달 로테이션에 참석하지 않은 사서입니다.", 125 | }); 126 | } 127 | } catch (error) { 128 | console.log(error); 129 | return res.status(500).json({ message: "참석자 삭제 실패." }); 130 | } 131 | } 132 | 133 | async function setRotation() { 134 | const monthArrayInfo = await rotationUtils.initMonthArray(); 135 | let month = monthArrayInfo.nextMonth; 136 | if (month < 10) { 137 | month = "0" + month; 138 | } 139 | let year = monthArrayInfo.year; 140 | try { 141 | let participants = await rotationRepository.getParticipants({ 142 | month: month, 143 | year: year, 144 | }); 145 | for (let i = 0; i < participants.length; i++) { 146 | try { 147 | await rotationRepository.initAttendInfo({ 148 | intraId: participants[i].intraId, 149 | month: month, 150 | year: year, 151 | }); 152 | } catch (error) { 153 | console.log(error); 154 | return { status: -1, info: error }; 155 | } 156 | } 157 | const rotationResult = await rotationUtils.checkAttend( 158 | rotationUtils.setRotation(participants, monthArrayInfo), 159 | ); 160 | if (rotationResult.status === false) { 161 | return { status: -1, info: rotationResult.message }; 162 | } 163 | for (let i = 0; i < rotationResult.monthArray.monthArray.length; i++) { 164 | for (let j = 0; j < rotationResult.monthArray.monthArray[i].length; j++) { 165 | for ( 166 | let k = 0; 167 | k < rotationResult.monthArray.monthArray[i][j].arr.length; 168 | k++ 169 | ) { 170 | if (rotationResult.monthArray.monthArray[i][j].arr[k] != "0") { 171 | let day = rotationResult.monthArray.monthArray[i][j].day; 172 | if (day < 10) { 173 | day = "0" + day; 174 | } 175 | let attendDate = year + "-" + month + "-" + day + ","; 176 | let participantId = 177 | rotationResult.monthArray.monthArray[i][j].arr[k]; 178 | try { 179 | await rotationRepository.setAttendDate({ 180 | attendDate: attendDate, 181 | isSet: 1, 182 | intraId: participantId, 183 | month: month, 184 | year: year, 185 | }); 186 | } catch (error) { 187 | console.log(error); 188 | return { status: -1, info: error }; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | return { status: 200, info: 'setRotation success' }; 195 | } catch (error) { 196 | console.log(error); 197 | return { status: -1, info: error }; 198 | } 199 | } 200 | 201 | export async function getRotationInfo(req, res) { 202 | if (!Object.keys(req.query).length) { 203 | try { 204 | let rotationInfo = await rotationRepository.getRotationInfo(); 205 | return res.status(200).json(rotationInfo); 206 | } catch (error) { 207 | console.log(error); 208 | return res.status(500).json({ message: "사서 로테이션 조회 실패" }); 209 | } 210 | } else { 211 | try { 212 | let year = new Date().getFullYear(); 213 | let month = (new Date().getMonth() % 12) + 1; 214 | if (month < 10) { 215 | month = "0" + month; 216 | } 217 | if ( 218 | Object.keys(req.query).indexOf("month") > -1 && 219 | !isNaN(parseInt(req.query.month, 10)) 220 | ) 221 | month = req.query.month; 222 | if ( 223 | Object.keys(req.query).indexOf("year") > -1 && 224 | !isNaN(parseInt(req.query.year, 10)) 225 | ) 226 | year = req.query.year; 227 | 228 | let participants = await rotationRepository.getParticipants({ 229 | month: month, 230 | year: year, 231 | }); 232 | let participantInfo = []; 233 | for (let i = 0; i < participants.length; i++) { 234 | let date = participants[i].attendDate.split(",").slice(0, -1); 235 | let participantId = participants[i].intraId; 236 | participantInfo.push({ date: date, intraId: participantId }); 237 | } 238 | return res.status(200).json(participantInfo); 239 | } catch (error) { 240 | console.log(error); 241 | return res.status(500).json({ message: "사서 로테이션 조회 실패" }); 242 | } 243 | } 244 | } 245 | 246 | export async function updateAttendInfo(req, res) { 247 | let intraId = req.body.intraId; 248 | let before = req.body.before.trim(); 249 | let after = req.body.after.trim(); 250 | 251 | let year = after.split("-")[0]; 252 | let month = after.split("-")[1]; 253 | try { 254 | const participantInfo = await rotationRepository.getParticipantInfo({ 255 | intraId: intraId, 256 | month: month, 257 | year: year, 258 | }); 259 | if (participantInfo.length === 0) { 260 | await rotationRepository.putParticipant({ 261 | intraId: intraId, 262 | attendLimit: [], 263 | month: month, 264 | year: year, 265 | attendDate: after + ",", 266 | }); 267 | return res 268 | .status(200) 269 | .json({ intraId: intraId, message: "PUT PARTICIPATION OK" }); 270 | } 271 | let attendDates = participantInfo[0].attendDate.split(",").slice(0, -1); 272 | let isSet = participantInfo[0].isSet; 273 | let newDates = []; 274 | if (before === "") { 275 | if (attendDates.indexOf(after) < 0) { 276 | await rotationRepository.setAttendDate({ 277 | attendDate: after + ",", 278 | intraId: intraId, 279 | month: month, 280 | year: year, 281 | isSet: isSet, 282 | }); 283 | } 284 | } else { 285 | for (let i = 0; i < attendDates.length; i++) { 286 | if (attendDates[i].trim() === before) { 287 | if (newDates.indexOf(after) < 0) newDates.push(after); 288 | } else { 289 | if (newDates.indexOf(attendDates[i].trim())) 290 | newDates.push(attendDates[i].trim()); 291 | } 292 | } 293 | await rotationRepository.updateAttendDate({ 294 | attendDate: newDates.join(",") + ",", 295 | intraId: intraId, 296 | month: month, 297 | year: year, 298 | }); 299 | } 300 | return res 301 | .status(200) 302 | .json({ intraId: intraId, message: "UPDATE ATTEND DATE OK" }); 303 | } catch (error) { 304 | console.log(error); 305 | return res.status(500).json({ message: "일정 업데이트 실패" }); 306 | } 307 | } 308 | 309 | export async function deleteAttendInfo(req, res) { 310 | let intraId = req.body.intraId; 311 | let dateDelete = req.body.date.trim(); 312 | 313 | let year = dateDelete.split("-")[0]; 314 | let month = dateDelete.split("-")[1]; 315 | try { 316 | const participantInfo = await rotationRepository.getParticipantInfo({ 317 | intraId: intraId, 318 | month: month, 319 | year: year, 320 | }); 321 | if (participantInfo.length === 0) { 322 | return res.status(400).json({ 323 | intraId: intraId, 324 | month: month, 325 | message: "해당 달 사서 업무에 참여하지 않은 사서입니다", 326 | }); 327 | } 328 | let attendDates = participantInfo[0].attendDate.split(",").slice(0, -1); 329 | let newDates = []; 330 | for (let i = 0; i < attendDates.length; i++) { 331 | if (attendDates[i].trim() != dateDelete) { 332 | newDates.push(attendDates[i].trim()); 333 | } 334 | } 335 | await rotationRepository.updateAttendDate({ 336 | attendDate: newDates.join(",") + ",", 337 | intraId: intraId, 338 | month: month, 339 | year: year, 340 | }); 341 | return res.status(200).json({ 342 | intraId: intraId, 343 | delete: dateDelete, 344 | message: "DELETE ATTEND DATE OK", 345 | }); 346 | } catch (error) { 347 | console.log(error); 348 | return res.status(500).json({ message: "사서 일정 삭제 실패" }); 349 | } 350 | } 351 | 352 | export async function postRotationMessage() { 353 | const fourthWeekDays = getFourthWeekdaysOfMonth(); 354 | let fourthWeekStartDay = fourthWeekDays[0]; 355 | let fourthWeekFifthDay = fourthWeekDays[4]; 356 | let fourthWeekEndDay = fourthWeekDays[fourthWeekDays.length - 1]; 357 | let today = new Date().getDate(); 358 | try { 359 | if (today === fourthWeekStartDay || today == fourthWeekFifthDay) { 360 | let str = `#42seoul_club_42jiphyeonjeon_ 마감 ${ 361 | fourthWeekEndDay - today 362 | }일 전! 사서 로테이션 신청 기간입니다. 친바 홈페이지에서 사서 로테이션 신청을 해주세요! https://together.42jip.net/`; 363 | await publishMessage(config.slack.jip, str); 364 | } else { 365 | const today = new Date(); 366 | const year = today.getFullYear(); 367 | const month = (today.getMonth() % 12) + 1; 368 | const lastDay = new Date(year, month, 0).getDate(); 369 | if (today === lastDay) { 370 | const str = `#42seoul_club_42jiphyeonjeon_ 다음 달 사서 로테이션이 완료되었습니다. 친바 홈페이지에서 확인해주세요! https://together.42jip.net/`; 371 | await publishMessage(config.slack.jip, str); 372 | 373 | // 다음달 로테이션 결과 사서 DM으로 전송 374 | const nextMonth = month + 1 === 13 ? 1 : month + 1; 375 | const nextMonthYear = month + 1 === 13 ? year + 1 : year; 376 | await postSlackMonthlyLibrarian(nextMonthYear, nextMonth); 377 | } 378 | } 379 | return { status: 200 }; 380 | } catch (error) { 381 | console.log(error); 382 | return { status: -1 }; 383 | } 384 | } 385 | 386 | export async function getUserParticipation(req, res) { 387 | let rotationInfo; 388 | const { intraId, month, year } = req.query; 389 | const obj = {}; 390 | const isValid = { 391 | intraId: (intraId) => intraId != undefined && intraId.length > 0, 392 | month: (month) => /^(1[0-2]|0?[1-9])$/.test(month), 393 | year: (year) => 394 | /^[0-9]{4}$/.test(year) && 395 | 2023 <= year && 396 | year <= new Date().getFullYear() + 1, 397 | }; 398 | if (isValid.intraId(intraId)) { 399 | obj.intraId = intraId; 400 | } 401 | if (isValid.month(month)) { 402 | obj.month = month; 403 | } 404 | if (isValid.year(year)) { 405 | obj.year = year; 406 | } 407 | 408 | try { 409 | if (!("intraId" in obj)) { 410 | rotationInfo = await rotationRepository.getRotationInfo(); 411 | } else if ("year" in obj && "month" in obj) { 412 | rotationInfo = await rotationRepository.getParticipantInfo(obj); 413 | } else if ("year" in obj) { 414 | rotationInfo = await rotationRepository.getParticipantInfoAllYear(obj); 415 | } else if ("month" in obj) { 416 | rotationInfo = await rotationRepository.getParticipantInfoAllMonth(obj); 417 | } else { 418 | rotationInfo = await rotationRepository.getParticipantInfoAll(obj); 419 | } 420 | return res.status(200).json(rotationInfo); 421 | } catch (error) { 422 | console.log(error); 423 | return res.status(500).json({ message: "사서 로테이션 DB 조회 실패" }); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/controller/slack.controller.js: -------------------------------------------------------------------------------- 1 | import { WebClient } from "@slack/web-api"; 2 | import { config } from "../config.js"; 3 | 4 | const token = config.slack.slack_token; 5 | const web = new WebClient(token); 6 | 7 | export const publishMessage = async (slackId, msg) => { 8 | await web.chat 9 | .postMessage({ 10 | token, 11 | channel: slackId, 12 | text: msg, 13 | }) 14 | .catch((e) => { 15 | console.log(e); 16 | }); 17 | }; 18 | 19 | export async function publishMessages(req, res) { 20 | const slackId = req.body.slackId; 21 | const msg = req.body.msg; 22 | await publishMessage(slackId, msg); 23 | res.sendStatus(204); 24 | } 25 | -------------------------------------------------------------------------------- /src/controller/timeline.controller.js: -------------------------------------------------------------------------------- 1 | import * as timelineRepository from "../data/timeline.js"; 2 | 3 | export async function listAllImages(req, res) { 4 | try { 5 | const images = await timelineRepository.listAllImages(); 6 | if (!images) { 7 | return res.status(400).json({ message: "이미지 조회 실패" }); 8 | } 9 | const imageUrl = images.map((item) => { 10 | return { 11 | url: item.filePath, 12 | }; 13 | }); 14 | return res.status(200).json({ img: imageUrl }); 15 | } catch (error) { 16 | console.log(error); 17 | return res.status(400).json({ message: "이미지 조회 실패" }); 18 | } 19 | } 20 | 21 | export async function upload(req, res) { 22 | const boardId = 38; 23 | const image = req.files; 24 | const path = image.map((img) => img.location); 25 | if (req.fileValidationError) { 26 | //파일이 크거나 형식이 다를때 27 | return res.status(400).send({ message: req.fileValidationError }); 28 | } 29 | if (image.length < 1) { 30 | //이미지가 없을때 31 | return res.status(400).send(util.fail(400, "이미지가 없습니다")); 32 | } 33 | const imageId = await timelineRepository.imageUpload(boardId, image); 34 | if (imageId.errno) 35 | return res.status(400).send({ message: "잘못된 boardId입니다" }); 36 | return res 37 | .status(200) 38 | .send( 39 | util.success(200, "업로드를 완료했습니다", { 40 | imageId: imageId, 41 | path: path, 42 | }), 43 | ); 44 | } 45 | 46 | const util = { 47 | success: (status, message, data) => { 48 | return { 49 | status: status, 50 | success: true, 51 | message: message, 52 | data: data, 53 | }; 54 | }, 55 | fail: (status, message) => { 56 | return { 57 | status: status, 58 | success: false, 59 | message: message, 60 | }; 61 | }, 62 | }; -------------------------------------------------------------------------------- /src/controller/together.controller.js: -------------------------------------------------------------------------------- 1 | import * as togetherRepository from "../data/together.js"; 2 | import * as userRepository from "../data/auth.js"; 3 | import { publishMessage } from "./slack.controller.js"; 4 | import { config } from "../config.js"; 5 | 6 | //이벤트 생성 7 | export async function createEvent(req, res) { 8 | const { title, description, categoryId } = req.body; 9 | const user = await userRepository.findById(req.userId); 10 | console.log(user); 11 | const createdId = user.id; 12 | const event = await togetherRepository.createEvent({ 13 | title, 14 | description, 15 | createdId, 16 | categoryId, 17 | }); 18 | let str = `:fire: 친바 공지 !! :fire:\n\n${title} 이벤트가 생성되었습니다. \nhttps://together.42jip.net/\n서둘러 참석해주세요`; 19 | await publishMessage(config.slack.jip, str); 20 | res.status(201).json({ event }); 21 | } 22 | 23 | // 주간회의 이벤트 자동 생성 24 | export async function createWeeklyDinnerEvent() { 25 | const adminUserIntraId = "tkim"; 26 | const title = "[주간 식사] 오늘 주간 회의 끝나고 같이 저녁 드실 분~"; 27 | const description = "같이 회의도 하고 식사도 하면서 친해집시다!"; 28 | const categoryId = 1; 29 | const adminUser = await userRepository.findByintraId(adminUserIntraId); 30 | console.log(adminUser); 31 | const createdId = adminUser.id; 32 | await togetherRepository.createEvent({ 33 | title, 34 | description, 35 | createdId, 36 | categoryId, 37 | }); 38 | const message = `:fire: 친바 공지 !! :fire:\n\n${title}\n이벤트가 생성되었습니다.\n금일 오후 6시에 자동 마감되오니 서둘러 참석해 주세요!\nhttps://together.42jip.net`; 39 | await publishMessage(config.slack.jip, message); 40 | } 41 | 42 | //이벤트 삭제 43 | export async function deleteEvent(req, res) { 44 | const id = req.params.id; 45 | const deleteId = await togetherRepository.findByEventId(id); 46 | const user = await userRepository.findById(req.userId); 47 | const createUser = user.id; 48 | 49 | if (!deleteId) 50 | //삭제할 친바가 없다면 51 | return res.status(404).json({ message: "이벤트가 없습니다" }); 52 | //권한 53 | console.log(deleteId); 54 | if (deleteId.createdId !== createUser && !req.isAdmin) 55 | return res.status(401).json({ message: "권한이 없습니다" }); 56 | 57 | await togetherRepository.deleteEvent(id); 58 | res.sendStatus(204); 59 | } 60 | 61 | //전체 이벤트 조회 62 | export async function getEventList(req, res) { 63 | const EventList = await togetherRepository.getEventList(); 64 | res.status(200).json({ EventList }); 65 | } 66 | 67 | //이벤트 상세조회 , 유저객체정보를 배열로 넘겨달라 68 | export async function getEvent(req, res) { 69 | const id = req.params.id; 70 | const event = await togetherRepository.findByEventId(id); 71 | if (!event) 72 | //조회할 친바가 없다면 73 | return res.status(404).json({ message: "이벤트가 없습니다" }); 74 | const teamList = await getTeamList(id); 75 | 76 | res.status(200).json({ event, teamList }); 77 | } 78 | 79 | //이벤트 참석 80 | export async function register(req, res) { 81 | const user = await userRepository.findById(req.userId); //토큰으로 받아온 아이디 82 | const eventId = req.body.eventId; 83 | const alreadyAttend = await togetherRepository.findByAttend(user.id, eventId); 84 | const matchCheck = await togetherRepository.findByEventId(eventId); 85 | 86 | if (matchCheck.isMatching == 1) 87 | //이미 매칭됐다면 88 | return res.status(400).json({ message: "already matching" }); 89 | if (alreadyAttend) 90 | //이미 참석했다면 91 | return res.status(400).json({ message: "이미 참석했습니다" }); 92 | const attend = await togetherRepository.register(user.id, eventId); 93 | console.log(`${user.intraId}가 ${eventId}에 참석하다`); 94 | res.status(201).json({ attend }); 95 | } 96 | 97 | //이벤트 참석해제 98 | export async function unregister(req, res) { 99 | const user = await userRepository.findById(req.userId); //토큰으로 받아온 아이디 100 | const eventId = req.params.id; //event id 101 | const alreadyAttend = await togetherRepository.findByAttend(user.id, eventId); 102 | if (!alreadyAttend) 103 | //참석이 없으면 104 | return res.status(400).json({ message: "참석자가 없습니다" }); 105 | //teamId가 있으면(즉 팀 매칭이 완료된경우) 106 | if (alreadyAttend.teamId !== null) 107 | return res.status(400).json({ message: "이미 매칭된 이벤트입니다" }); 108 | 109 | await togetherRepository.unregister(user.id, eventId); 110 | console.log(`${user.intraId}가 ${eventId}에 참석 취소하다`); 111 | res.sendStatus(204); 112 | } 113 | 114 | async function getTeamList(id) { 115 | //중복되는 부분이여서 함수로빼냄 116 | const matchingList = await togetherRepository.getMatchingList(id); 117 | //teamId(키)로 객체에서 배열 그룹화 118 | let teamList = matchingList.reduce(function (r, a) { 119 | r[a.teamId] = r[a.teamId] || []; 120 | r[a.teamId].push(a); 121 | return r; 122 | }, Object.create(null)); 123 | return teamList; 124 | } 125 | 126 | export async function getTeam(req, res) { 127 | const id = req.params.id; //event id 128 | const teamList = await getTeamList(id); 129 | res.status(200).json({ teamList }); 130 | } 131 | 132 | export async function matching(req, res) { 133 | const { eventId, teamNum } = req.body; 134 | const create = await togetherRepository.findCreateUser(eventId); 135 | if (create.createdId !== req.userId && !req.isAdmin) 136 | return res.status(400).json({ message: "권한이 없습니다" }); 137 | const eventTitle = await togetherRepository.getByEventTitle(eventId); 138 | const check = await togetherRepository.findAttendByEventId(eventId); 139 | if (check === undefined || check[0] === undefined || check[0].teamId !== null) 140 | //참석자가 없거나, 이미 매칭이 된경우 141 | return res 142 | .status(400) 143 | .json({ message: "참석자가 없거나 이미 매칭됐습니다" }); 144 | if (check.length < teamNum) 145 | //유저보다 팀 개수가 많을때 146 | return res.status(400).json({ message: "팀 개수가 너무 많습니다" }); 147 | if (teamNum === 0 || teamNum === "0") 148 | //유저보다 팀 개수가 많을때 149 | return res.status(400).json({ message: "팀 개수 0개 입니다" }); 150 | shuffle(check); //팀 셔플완료 이제 팀개수대로 팀 나눠야함 151 | await togetherRepository.changeEvent(eventId); 152 | for (let i = 0; i < check.length; i++) { 153 | let teamId = (i % teamNum) + 1; 154 | await togetherRepository.createTeam(teamId, check[i].id); 155 | } 156 | const teamList = await getTeamList(eventId); 157 | console.log(eventTitle); 158 | let str = `:fire: 친바 공지 !! :fire:\n\n[${eventTitle.title}] 팀 매칭이 완료되었습니다.\n서둘러 자신의 팀원들과 연락해보세요!\nhttps://together.42jip.net/ \n :ultra_fast_parrot:\n\n`; 159 | for (let key in teamList) { 160 | const value = teamList[key]; 161 | const test = value.map((team) => team.intraId); 162 | str += key; 163 | str += "팀 : "; 164 | str += test.join(", "); 165 | str += "\n"; 166 | } 167 | console.log(`문자열 출력 ${str}`); 168 | await publishMessage(config.slack.ywee, str); 169 | res.status(201).json({ eventId, teamList }); 170 | } 171 | 172 | export async function matchWeeklyDinnerEvent() { 173 | const event = await togetherRepository.getNotMatchedEventByCategory(1); 174 | if (event === undefined) return; 175 | const check = await togetherRepository.findAttendByEventId(event.id); 176 | // 참석자가 없는 경우 177 | if (check === undefined || check[0] === undefined) { 178 | await togetherRepository.deleteEvent(event.id); 179 | return; 180 | } 181 | // 이미 매칭된 경우 182 | if (check[0].teamId !== null) return; 183 | // 매칭 되지 않은 경우, 매칭 상태로 변경 isMatching=1 184 | await togetherRepository.changeEvent(event.id); 185 | for (let i = 0; i < check.length; i++) { 186 | // 팀은 무조건 하나로만 자동 매칭 187 | await togetherRepository.createTeam(1, check[i].id); 188 | } 189 | let message = `:fire: 친바 공지 !! :fire:\n\n오늘 저녁 식사 모집 마감되었습니다.\n`; 190 | const attendees = await userRepository.findUsersByIdList( 191 | check.map((e) => e.userId), 192 | ); 193 | message += attendees.map((e) => "`" + e.intraId + "`").join(", "); 194 | message += " 님 맛있게 드세요!"; 195 | await publishMessage(config.slack.jip, message); 196 | } 197 | 198 | //팀 셔플 199 | function shuffle(array) { 200 | array.sort(() => Math.random() - 0.5); 201 | } 202 | 203 | //게시글 작성을 위한 정보 조회 (모든 이벤트, 팀리스트) 204 | export async function getEventInfo(req, res) { 205 | let eventList = await togetherRepository.getEventList(); 206 | for (let i = 0; i < eventList.length; i++) { 207 | const team = await getTeamList(eventList[i].id); 208 | eventList[i].teamList = team; 209 | } 210 | res.status(200).json(eventList); 211 | } 212 | 213 | export async function getAttendingPoint(req, res) { 214 | const pointList = await togetherRepository.getAttendingPoint(); 215 | res.status(200).json(pointList); 216 | } 217 | -------------------------------------------------------------------------------- /src/controller/user.controller.js: -------------------------------------------------------------------------------- 1 | import * as userRepository from "../data/user.js"; 2 | 3 | export async function getUserList(req, res) { 4 | const userList = await userRepository.getUserList(); 5 | if (!userList) { 6 | return res.status(404).json({ message: "사용자가 없습니다" }); 7 | } 8 | res.status(200).json({ userList: userList }); 9 | } 10 | -------------------------------------------------------------------------------- /src/data/auth.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function findById(id) { 4 | return db 5 | .execute("SELECT * FROM users WHERE id=?", [id]) 6 | .then((result) => result[0][0]); 7 | } 8 | 9 | export async function findByintraId(intraId) { 10 | return db 11 | .execute("SELECT * FROM users WHERE intraId=?", [intraId]) 12 | .then((result) => result[0][0]); 13 | } 14 | 15 | export async function findByEmail(email) { 16 | return db 17 | .execute("SELECT * FROM users WHERE email=?", [email]) 18 | .then((result) => result[0][0]); 19 | } 20 | 21 | export async function createUser(user) { 22 | const { intraId, password, email, profile } = user; 23 | return db 24 | .execute( 25 | "INSERT INTO users (intraId, password, email, profile) VALUES (?,?,?,?)", 26 | [intraId, password, email, profile], 27 | ) 28 | .then((result) => result[0].insertId); 29 | } 30 | 31 | export async function getByUserList() { 32 | return db 33 | .execute("SELECT id, intraId, profile FROM users") 34 | .then((result) => result[0]); 35 | } 36 | 37 | export async function updatePassword(userInfo) { 38 | const { id, intraId, password } = userInfo; 39 | return db 40 | .execute("UPDATE users SET password=? WHERE id=?", [password, id]) 41 | .then(() => findById(id)); 42 | } 43 | 44 | export async function findUsersByIdList(idList) { 45 | const placeholder = idList.map(() => "?"); 46 | return db 47 | .execute(`select intraId from users where id in (${placeholder})`, [ 48 | ...idList, 49 | ]) 50 | .then((result) => result[0]); 51 | } 52 | -------------------------------------------------------------------------------- /src/data/board.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function findByPostId(id) { 4 | return db 5 | .execute("SELECT * FROM board WHERE id=?", [id]) 6 | .then((result) => result[0][0]); 7 | } 8 | 9 | export async function deletePost(id) { 10 | return db.execute("DELETE FROM board WHERE id=?", [id]); 11 | } 12 | 13 | export async function deleteComment(id) { 14 | return db.execute("DELETE FROM board_comment WHERE id=?", [id]); 15 | } 16 | 17 | export async function createPost(post) { 18 | const { writerId, title, contents, eventId } = post; 19 | return db 20 | .execute( 21 | "INSERT INTO board (writerId, title, contents, eventId) VALUES (?,?,?,?)", 22 | [writerId, title, contents, eventId], 23 | ) 24 | .then((result) => result[0].insertId); 25 | } 26 | 27 | export async function createComment(boardId, comment, writerId) { 28 | return db 29 | .execute( 30 | "INSERT INTO board_comment (boardId, comments, writerId) VALUES (?,?,?)", 31 | [boardId, comment, writerId], 32 | ) 33 | .then((result) => result[0].insertId); 34 | } 35 | 36 | export async function updatePost(post) { 37 | const { id, title, contents, eventId } = post; 38 | return db 39 | .execute("UPDATE board SET title=? ,contents=? ,eventId=? WHERE id=?", [ 40 | title, 41 | contents, 42 | eventId, 43 | id, 44 | ]) 45 | .then(() => findByPostId(id)); 46 | } 47 | 48 | export async function updateComment(comment, id) { 49 | return db 50 | .execute("UPDATE board_comment SET comments = ? WHERE id=?", [comment, id]) 51 | .then(() => findByCommentId(id)); 52 | } 53 | 54 | export async function getBoardList(eventId) { 55 | let query; 56 | if (eventId) { 57 | query = ` 58 | SELECT 59 | board.id as boardId, 60 | board.eventId, 61 | board.title, 62 | us.intraId, 63 | board.contents, 64 | board.createdAt, 65 | board.updatedAt, 66 | count(board_comment.id) as commentNum, 67 | us.profile 68 | FROM board 69 | LEFT JOIN users as us ON board.writerId = us.id 70 | LEFT JOIN board_comment ON board.id=board_comment.boardId 71 | WHERE board.eventId = ${eventId} 72 | GROUP BY board.id;`; 73 | } else { 74 | query = ` 75 | SELECT 76 | board.id as boardId, 77 | board.eventId, 78 | board.title, 79 | us.intraId, 80 | board.contents, 81 | board.createdAt, 82 | board.updatedAt, 83 | count(board_comment.id) as commentNum, 84 | us.profile 85 | FROM board 86 | LEFT JOIN users as us ON board.writerId = us.id 87 | LEFT JOIN board_comment ON board.id=board_comment.boardId 88 | GROUP BY board.id; 89 | `; 90 | } 91 | return db.query(`${query}`).then((result) => result[0]); 92 | } 93 | 94 | export async function getBoard(boardId) { 95 | return db 96 | .query( 97 | ` 98 | SELECT 99 | board.id as boardId, 100 | board.eventId, 101 | board.title, 102 | us.intraId, 103 | board.contents, 104 | board.createdAt, 105 | board.updatedAt, 106 | us.profile 107 | FROM board 108 | LEFT JOIN users as us ON board.writerId = us.id 109 | WHERE board.id = ? 110 | `, 111 | [boardId], 112 | ) 113 | .then((result) => result[0][0]); 114 | } 115 | 116 | export async function getAttendMembers(boardId) { 117 | return db 118 | .query( 119 | ` 120 | SELECT users.intraId, users.profile FROM board_attend_members as bam JOIN users ON users.intraId=bam.intraId WHERE boardId = ? 121 | `, 122 | [boardId], 123 | ) 124 | .then((result) => result[0]); 125 | } 126 | 127 | export async function getComments(boardId) { 128 | return db 129 | .query( 130 | ` 131 | SELECT 132 | bc.id, 133 | users.intraId, 134 | bc.comments, 135 | bc.updatedAt 136 | FROM board_comment as bc 137 | JOIN users ON users.id=bc.writerId 138 | WHERE boardId = ? 139 | ; 140 | `, 141 | [boardId], 142 | ) 143 | .then((result) => result[0]); 144 | } 145 | 146 | export async function createAttendMember(members, boardId) { 147 | const values = members.map((member) => { 148 | return [member.intraId, boardId]; 149 | }); 150 | console.log(values); 151 | return db 152 | .query("INSERT INTO board_attend_members (intraId,boardId) VALUES ?", [ 153 | values, 154 | ]) 155 | .then((result) => result[0]); 156 | } 157 | export async function checkAttendMember(members) { 158 | const values = members.map((member) => { 159 | return [`"${member.intraId}"`]; 160 | }); 161 | const sql = `SELECT intraId, slackId FROM users WHERE intraId IN (${values.toString()})`; 162 | return db.query(sql).then((result) => result[0]); 163 | } 164 | 165 | export async function findByCommentId(id) { 166 | return db 167 | .execute("SELECT * FROM board_comment WHERE id=?", [id]) 168 | .then((result) => result[0][0]); 169 | } 170 | 171 | //upload 172 | 173 | export async function imageUpload(boardId, images) { 174 | const values = images.map((image) => { 175 | return [ 176 | boardId, 177 | image.location, 178 | image.originalname, 179 | image.mimetype, 180 | image.size, 181 | image.key, 182 | ]; 183 | }); 184 | console.log(`value : ${values}`); 185 | return db 186 | .query( 187 | "INSERT INTO image_info (boardNum, filePath, fileName, fileType, fileSize, fileKey) VALUES ?", 188 | [values], 189 | ) 190 | .then((result) => result[0].insertId) 191 | .catch((error) => error); 192 | } 193 | 194 | export async function getImages(boardId) { 195 | //console.log(`getImage = ${boardId}`); 196 | return db 197 | .query( 198 | ` 199 | SELECT id as imageId, boardNum as boardId, filePath, fileType, fileKey FROM image_info WHERE boardNum = ? 200 | `, 201 | [boardId], 202 | ) 203 | .then((result) => result[0]); 204 | } 205 | 206 | export async function findByImageId(id) { 207 | return db 208 | .execute("SELECT * FROM image_info WHERE id=?", [id]) 209 | .then((result) => result[0][0]); 210 | } 211 | 212 | export async function deleteImage(id) { 213 | return db.execute("DELETE FROM image_info WHERE id=?", [id]); 214 | } 215 | -------------------------------------------------------------------------------- /src/data/rotation.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function getAttendableUsers() { 4 | try { 5 | return db 6 | .execute("SELECT * FROM users WHERE canAttend='1'", 7 | ) 8 | .then((result) => result[0]); 9 | } catch (error) { 10 | throw error; 11 | } 12 | } 13 | 14 | export async function addParticipant(participant) { 15 | const { intraId, attendLimit, month, year } = participant; 16 | return db 17 | .execute("INSERT INTO rotation (intraId, attendLimit, month, year, isSet) VALUES (?,?,?,?,?)", 18 | [intraId, attendLimit, month, year, 0], 19 | ) 20 | .then((result) => result[0].insertId); 21 | } 22 | 23 | export async function putParticipant(participant) { 24 | const { intraId, attendLimit, month, year, attendDate } = participant; 25 | return db 26 | .execute("INSERT INTO rotation (intraId, attendLimit, month, year, attendDate, isSet) VALUES (?,?,?,?,?,?)", 27 | [intraId, attendLimit, month, year, attendDate, 0], 28 | ) 29 | .then((result) => result[0].insertId); 30 | } 31 | 32 | export async function deleteParticipant(participantInfo) { 33 | return db 34 | .execute("DELETE FROM rotation WHERE (intraId=? AND month=? AND year=?)", 35 | [participantInfo.intraId, participantInfo.month, participantInfo.year], 36 | ) 37 | .then((result) => result[0]); 38 | } 39 | 40 | export async function getParticipants(dateInfo) { 41 | return db 42 | .execute("SELECT id, intraId, attendLimit, attendDate, isSet FROM rotation WHERE (month=? AND year=?)", 43 | [dateInfo.month, dateInfo.year], 44 | ) 45 | .then((result) => result[0]); 46 | } 47 | 48 | export async function getRotationInfo() { 49 | return db 50 | .execute("SELECT * FROM rotation") 51 | .then((result) => result[0]); 52 | } 53 | 54 | export async function getParticipantInfo(participantInfo) { 55 | return db 56 | .execute("SELECT id, intraId, attendLimit, attendDate, isSet from rotation WHERE (intraId=? AND month=? AND year=?)", 57 | [participantInfo.intraId, participantInfo.month, participantInfo.year], 58 | ) 59 | .then((result) => result[0]); 60 | } 61 | 62 | export async function getParticipantInfoAll(participantInfo) { 63 | return db 64 | .execute("SELECT id, intraId, attendLimit, attendDate, isSet from rotation WHERE (intraId=?)", [participantInfo.intraId], 65 | ) 66 | .then((result) => result[0]); 67 | } 68 | 69 | export async function getParticipantInfoAllMonth(participantInfo) { 70 | return db 71 | .execute("SELECT id, intraId, attendLimit, attendDate, isSet from rotation WHERE (intraId=? AND month=?)", 72 | [participantInfo.intraId, participantInfo.month], 73 | ) 74 | .then((result) => result[0]); 75 | } 76 | 77 | export async function getParticipantInfoAllYear(participantInfo) { 78 | return db 79 | .execute("SELECT id, intraId, attendLimit, attendDate, isSet from rotation WHERE (intraId=? AND year=?)", 80 | [participantInfo.intraId, participantInfo.year], 81 | ) 82 | .then((result) => result[0]); 83 | } 84 | 85 | export async function setAttendDate(attendInfo) { 86 | return db 87 | .execute("UPDATE rotation SET attendDate=CONCAT(IFNULL(attendDate, ''),?),isSet=? WHERE (intraId=? AND month=? AND year=?)", 88 | [attendInfo.attendDate, attendInfo.isSet, attendInfo.intraId, attendInfo.month, attendInfo.year], 89 | ) 90 | .then((result) => result[0]); 91 | } 92 | 93 | export async function initAttendInfo(attendInfo) { 94 | return db 95 | .execute("UPDATE rotation SET attendDate='',isSet=? WHERE (intraId=? AND month=? and year=?)", 96 | [0, attendInfo.intraId, attendInfo.month, attendInfo.year], 97 | ) 98 | .then((result) => result[0]); 99 | } 100 | 101 | export async function updateAttendDate(attendInfo) { 102 | return db 103 | .execute("UPDATE rotation SET attendDate=? WHERE (intraId=? AND month=? and year=?)", 104 | [attendInfo.attendDate, attendInfo.intraId, attendInfo.month, attendInfo.year], 105 | ) 106 | .then((result) => result[0]); 107 | } 108 | 109 | export async function addHolidayInfo(holidayInfo) { 110 | const { year, month, day, info } = holidayInfo; 111 | try { 112 | const [rows] = await db.execute( 113 | "SELECT * FROM holiday_info WHERE year = ? AND month = ? AND day = ?", 114 | [year, month, day], 115 | ); 116 | if (rows.length === 0) { 117 | return db 118 | .execute("INSERT INTO holiday_info (month, year, day, info) VALUES (?,?,?,?)", 119 | [month, year, day, info], 120 | ) 121 | .then((result) => result[0]); 122 | } else { 123 | console.log('Record already exists'); 124 | } 125 | } catch (error) { 126 | throw error; 127 | } 128 | } 129 | 130 | export async function getHolidayByMonth(holidayInfo) { 131 | const { year, month } = holidayInfo; 132 | try { 133 | return db 134 | .execute("SELECT * FROM holiday_info WHERE year = ? AND month = ?", 135 | [year, month], 136 | ) 137 | .then((result) => result[0]); 138 | } catch (error) { 139 | throw error; 140 | } 141 | } 142 | 143 | export async function getLibariansByDate(date) { 144 | console.log(date); 145 | try { 146 | return db 147 | .execute( 148 | "SELECT users.intraId, users.slackId, rotation.attendDate \ 149 | FROM users, rotation \ 150 | WHERE users.intraId = rotation.intraId and rotation.attendDate LIKE ?", 151 | [`%${date}%`], 152 | ) 153 | .then((result) => result[0]); 154 | } catch (error) { 155 | console.log(error); 156 | throw error; 157 | } 158 | } 159 | 160 | 161 | export async function getMonthlyLibarians(year, month) { 162 | try { 163 | return db 164 | .execute( 165 | "SELECT users.intraId, users.slackId, rotation.attendDate \ 166 | FROM users, rotation \ 167 | WHERE users.intraId = rotation.intraId and year = ? and month = ?", 168 | [year, month], 169 | ) 170 | .then((result) => result[0]); 171 | } catch (error) { 172 | console.log(error); 173 | return []; 174 | } 175 | } -------------------------------------------------------------------------------- /src/data/timeline.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function listAllImages() { 4 | return db 5 | .execute( 6 | 'SELECT filePath FROM image_info WHERE (filekey REGEXP "^timeline*")', 7 | ) 8 | .then((result) => result[0]); 9 | } 10 | 11 | export async function imageUpload(boardId, images) { 12 | const values = images.map((image) => { 13 | return [ 14 | boardId, 15 | image.location, 16 | image.originalname, 17 | image.mimetype, 18 | image.size, 19 | image.key, 20 | ]; 21 | }); 22 | console.log(`value : ${values}`); 23 | return db 24 | .query( 25 | "INSERT INTO image_info (boardNum, filePath, fileName, fileType, fileSize, fileKey) VALUES ?", 26 | [values], 27 | ) 28 | .then((result) => result[0].insertId) 29 | .catch((error) => error); 30 | } -------------------------------------------------------------------------------- /src/data/together.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function getEventList() { 4 | return db 5 | .execute( 6 | "SELECT ev.id, ev.title, ev.description, ev.createdId, us.intraId, ev.isMatching, ev.categoryId FROM event_info as ev JOIN users as us ON ev.createdId=us.id", 7 | ) 8 | .then((result) => result[0]); 9 | } 10 | 11 | export async function getNotMatchedEventByCategory(categoryId) { 12 | return db 13 | .execute("SELECT * FROM event_info WHERE categoryId=? AND isMatching=0", [ 14 | categoryId, 15 | ]) 16 | .then((result) => result[0][0]) 17 | .catch(() => undefined); 18 | } 19 | 20 | export async function findByEventId(id) { 21 | return ( 22 | db 23 | //.execute('SELECT * FROM event_info WHERE id=?',[id]) 24 | .execute( 25 | "SELECT ev.id, ev.title, ev.description, ev.createdId, us.intraId, ev.isMatching FROM event_info as ev JOIN users as us ON ev.createdId=us.id WHERE ev.id=?", 26 | [id], 27 | ) 28 | .then((result) => result[0][0]) 29 | ); 30 | } 31 | 32 | export async function getByEventTitle(id) { 33 | return db 34 | .execute("SELECT title FROM event_info WHERE id=?", [id]) 35 | .then((result) => result[0][0]); 36 | } 37 | 38 | export async function deleteEvent(id) { 39 | return db.execute("DELETE FROM event_info WHERE id=?", [id]); 40 | } 41 | 42 | export async function createEvent(event) { 43 | const { title, description, createdId, categoryId } = event; 44 | return db 45 | .execute( 46 | "INSERT INTO event_info (title, description, createdId, categoryId) VALUES (?,?,?,?)", 47 | [title, description, createdId, categoryId], 48 | ) 49 | .then((result) => result[0].insertId); 50 | } 51 | 52 | export async function register(user, eventId) { 53 | return db 54 | .execute("INSERT INTO attendance_info (userId, eventId) VALUES (?,?)", [ 55 | user, 56 | eventId, 57 | ]) 58 | .then((result) => result[0].insertId); 59 | } 60 | 61 | export async function unregister(user, eventId) { 62 | return db.execute("DELETE FROM attendance_info WHERE userId=? && eventId=?", [ 63 | user, 64 | eventId, 65 | ]); 66 | } 67 | 68 | export async function findByAttend(user, eventId) { 69 | return db 70 | .execute("SELECT * FROM attendance_info WHERE userId=? && eventId=?", [ 71 | user, 72 | eventId, 73 | ]) 74 | .then((result) => result[0][0]); 75 | } 76 | 77 | export async function findAttendByEventId(eventId) { 78 | return db 79 | .execute("SELECT * FROM attendance_info WHERE eventId=?", [eventId]) 80 | .then((result) => result[0]); 81 | } 82 | 83 | export async function changeEvent(eventId) { 84 | return db.execute("UPDATE event_info SET isMatching=1 WHERE id=?", [eventId]); 85 | } 86 | 87 | export async function getMatchingList(id) { 88 | return db 89 | .execute( 90 | "SELECT us.intraId, us.profile, at.teamId from attendance_info as at JOIN users as us ON at.userId=us.id WHERE at.eventId=? ORDER BY at.teamId", 91 | [id], 92 | ) 93 | .then((result) => result[0]); 94 | } 95 | 96 | export async function findCreateUser(eventId) { 97 | return db 98 | .execute("SELECT * FROM event_info WHERE id=?", [eventId]) 99 | .then((result) => result[0][0]); 100 | } 101 | 102 | export async function getByAttendId(id) { 103 | return db 104 | .execute("SELECT * FROM attendance_info WHERE id=?", [id]) 105 | .then((result) => result[0][0]); 106 | } 107 | 108 | export async function createTeam(teamId, id) { 109 | return db 110 | .execute("UPDATE attendance_info SET teamId=? WHERE id=?", [teamId, id]) 111 | .then(() => getByAttendId(id)); 112 | } 113 | 114 | export async function getAttendingPoint() { 115 | return db 116 | .execute( 117 | `SELECT at.userId, us.intraId, us.profile, COUNT(at.userId) as totalPoint, 118 | COUNT(case when ev.categoryId = 1 then 1 end) as meetingPoint, 119 | COUNT(case when ev.categoryId = 2 then 1 end) as eventPoint 120 | FROM attendance_info as at 121 | JOIN users as us ON at.userId=us.id 122 | JOIN event_info as ev ON at.eventId=ev.id 123 | WHERE ev.isMatching=1 124 | GROUP BY at.userId 125 | ORDER BY totalPoint DESC; 126 | `, 127 | ) 128 | .then((result) => result[0]); 129 | } 130 | -------------------------------------------------------------------------------- /src/data/user.js: -------------------------------------------------------------------------------- 1 | import { db } from "../db/database.js"; 2 | 3 | export async function getUserList() { 4 | console.log(db.execute("SELECT intraId FROM users")); 5 | return db 6 | .execute("SELECT id, intraId, profile FROM users") 7 | .then((result) => result[0]); 8 | } 9 | 10 | export async function findUserById(id) { 11 | return db 12 | .execute("SELECT * FROM users WHERE id=?", [id]) 13 | .then((result) => result[0][0]); 14 | } 15 | -------------------------------------------------------------------------------- /src/db/database.js: -------------------------------------------------------------------------------- 1 | import { config } from "../config.js"; 2 | import mysql from "mysql2"; 3 | 4 | const pool = mysql.createPool({ 5 | host: config.db.host, 6 | port: config.db.port, 7 | user: config.db.user, 8 | database: config.db.database, 9 | password: config.db.password, 10 | dateStrings: "date", 11 | }); 12 | 13 | export const db = pool.promise(); 14 | -------------------------------------------------------------------------------- /src/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import * as userRepository from "../data/auth.js"; 3 | import { config } from "../config.js"; 4 | 5 | const AUTH_ERROR = { message: "Authetication Error" }; 6 | 7 | //모든 요청에 대해서 헤더에 auth 있는지 8 | export const isAuth = async (req, res, next) => { 9 | const authHeader = req.get("Authorization"); 10 | if (!(authHeader && authHeader.startsWith("Bearer "))) { 11 | return res.status(401).json(AUTH_ERROR); 12 | } 13 | 14 | const token = authHeader.split(" ")[1]; 15 | 16 | //추후 수정해야함 17 | jwt.verify(token, config.jwt.secretKey, async (error, decoded) => { 18 | if (error) { 19 | return res.status(401).json(AUTH_ERROR); 20 | } 21 | const user = await userRepository.findByintraId(decoded.id); 22 | if (!user) { 23 | return res.status(401).json(AUTH_ERROR); 24 | } 25 | req.userId = user.id; //req.customdData 26 | req.isAdmin = user.isAdmin; 27 | next(); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/middleware/rate-limiter.js: -------------------------------------------------------------------------------- 1 | import rateLimit from "express-rate-limit"; 2 | import { config } from "../config.js"; 3 | 4 | export default rateLimit({ 5 | windowMs: config.rateLimit.windowMs, 6 | max: config.rateLimit.maxRequest, 7 | }); 8 | -------------------------------------------------------------------------------- /src/middleware/uploads.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import multerS3 from "multer-s3"; 3 | import { s3 } from "../s3.js"; 4 | import { config } from "../config.js"; 5 | 6 | export const fileSizeLimitErrorHandler = (err, req, res, next) => { 7 | if (err) { 8 | res.status(400).send({ message: "파일의 최대 크기는 50MB입니다" }); 9 | } else { 10 | next(); 11 | } 12 | }; 13 | 14 | function isType(file) { 15 | return ( 16 | file.mimetype == "image/jpeg" || 17 | file.mimetype == "image/png" || 18 | file.mimetype == "image/gif" || 19 | file.mimetype == "video/mp4" || 20 | file.mimetype == "image/jpg" || 21 | file.mimetype == "image/svg+xml" || 22 | file.mimetype == "video/quicktime" 23 | ); 24 | } 25 | 26 | const fileFilter = (req, file, cb) => { 27 | // mime type 체크하여 이미지만 필터링 28 | if (isType(file)) { 29 | req.fileValidationError = null; 30 | cb(null, true); 31 | } else { 32 | req.fileValidationError = 33 | "jpeg, jpg, png, svg, gif, mp4, mov 파일만 업로드 가능합니다."; 34 | cb(null, false); 35 | } 36 | }; 37 | 38 | export const upload = multer( 39 | { 40 | storage: multerS3({ 41 | s3: s3, 42 | bucket: config.s3.bucket, 43 | acl: "public-read", 44 | key: function (req, file, cb) { 45 | cb(null, `uploads/${Date.now()}_${file.originalname}`); 46 | }, 47 | }), 48 | fileFilter: fileFilter, 49 | limits: { 50 | fileSize: 100 * 1024 * 1024, //50mb 51 | }, 52 | }, 53 | "NONE", 54 | ); 55 | 56 | export const timelineUpload = multer( 57 | { 58 | storage: multerS3({ 59 | s3: s3, 60 | bucket: config.s3.bucket, 61 | acl: "public-read", 62 | key: function (req, file, cb) { 63 | cb(null, `timeline/${Date.now()}_${file.originalname}`); 64 | }, 65 | }), 66 | fileFilter: fileFilter, 67 | limits: { 68 | fileSize: 100 * 1024 * 1024, //50mb 69 | }, 70 | }, 71 | "NONE", 72 | ); 73 | -------------------------------------------------------------------------------- /src/middleware/validator.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from "express-validator"; 2 | import { body } from "express-validator"; 3 | 4 | export const validate = (req, res, next) => { 5 | const errors = validationResult(req); 6 | if (errors.isEmpty()) { 7 | return next(); 8 | } 9 | return res.status(400).json({ message: errors.array()[0].msg }); 10 | }; 11 | 12 | //로그인할때 13 | export const validateCredential = [ 14 | body("intraId") 15 | .trim() 16 | .notEmpty() 17 | .isLength({ min: 2 }) 18 | .withMessage("intraId는 2글자 이상이어야 합니다"), 19 | body("password") 20 | .trim() 21 | .isLength({ min: 8 }) 22 | .withMessage("password는 8글자 이상이어야 합니다"), 23 | validate, 24 | ]; 25 | 26 | //회원가입 유효성 검사 27 | export const validateSignup = [ 28 | ...validateCredential, 29 | body("email") 30 | .isEmail() 31 | .normalizeEmail() 32 | .withMessage("email 형식을 지켜주세요"), 33 | validate, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { validateSignup } from "../middleware/validator.js"; 4 | import { isAuth } from "../middleware/auth.js"; 5 | import * as authController from "../controller/auth.controller.js"; 6 | 7 | const router = express.Router(); 8 | 9 | //POST /signUp 10 | router.post("/signup", validateSignup, authController.signUp); 11 | 12 | //POST /login 13 | router.post("/login", authController.login); 14 | 15 | //GET /me 16 | router.get("/me", isAuth, authController.me); 17 | 18 | //메일인증 보내기 19 | router.post("/mail", authController.mailAuthentication); 20 | 21 | //메일 인증확인 22 | router.post("/cert", authController.cert); 23 | 24 | //GET 유저리스트 조회 25 | router.get("/userList", authController.getByUserList); 26 | 27 | //유저 정보 조회(intraId, Email) 28 | router.get("/userInfo/:id", authController.getByUserInfo); 29 | 30 | // 유저 비밀번호 변경(intraId, password) 31 | router.put("/password/:id", authController.updatePassword); 32 | 33 | export default router; 34 | -------------------------------------------------------------------------------- /src/routes/board.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { isAuth } from "../middleware/auth.js"; 4 | import * as boardController from "../controller/board.controller.js"; 5 | import { fileSizeLimitErrorHandler, upload } from "../middleware/uploads.js"; 6 | 7 | const router = express.Router(); 8 | 9 | //게시글 전체조회 10 | router.get("/", boardController.getBoardList); 11 | 12 | //게시글 생성 13 | router.post("/", isAuth, boardController.createPost); 14 | 15 | //게시글 삭제 16 | router.delete("/:id", isAuth, boardController.deletePost); 17 | 18 | //게시글 수정 19 | router.put("/:id", isAuth, boardController.updatePost); 20 | 21 | //게시글 상세조회 22 | router.get("/:id", boardController.getBoardDetail); 23 | 24 | //댓글 생성 25 | router.post("/comment", isAuth, boardController.createComment); 26 | 27 | //댓글 수정 28 | router.put("/comment/:id", isAuth, boardController.updateComment); 29 | 30 | //댓글 삭제 31 | router.delete("/comment/:id", isAuth, boardController.deleteComment); 32 | 33 | //사진 업로드 34 | router.post( 35 | "/upload", 36 | upload.array("image", 10), 37 | fileSizeLimitErrorHandler, 38 | boardController.upload, 39 | ); 40 | 41 | //사진 삭제 42 | router.delete("/image/remove/:id", boardController.deleteImage); 43 | 44 | export default router; 45 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authRouter from "./auth.routes.js"; 3 | import togetherRouter from "./together.routes.js"; 4 | import boardRouter from "./board.routes.js"; 5 | import slackRouter from "./slack.routes.js"; 6 | import userRouter from "./user.routes.js"; 7 | import timelineRouter from "./timeline.routes.js"; 8 | import rotationRouter from "./rotation.routes.js"; 9 | 10 | const router = Router(); 11 | 12 | router.use("/auth", authRouter); 13 | router.use("/together", togetherRouter); 14 | router.use("/board", boardRouter); 15 | router.use("/slack", slackRouter); 16 | router.use("/user", userRouter); 17 | router.use("/timeline", timelineRouter); 18 | router.use("/rotation", rotationRouter); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /src/routes/rotation.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import * as rotationController from "../controller/rotation.controller.js"; 4 | import { isAuth } from "../middleware/auth.js"; 5 | 6 | const router = express.Router(); 7 | 8 | // 사서 로테이션 참석 기록 반환 9 | router.get("/attend", isAuth, rotationController.getUserParticipation); 10 | 11 | // 로테이션 참석 사서 추가 12 | router.post("/attend", isAuth, rotationController.addParticipant); 13 | 14 | // 로테이션 참석 사서 삭제 15 | router.delete("/attend", isAuth, rotationController.deleteParticipant); 16 | 17 | // 전체 사서 로테이션 결과 반환 18 | router.get("/", rotationController.getRotationInfo); 19 | 20 | // 사서 일정 변경 21 | router.patch("/update", isAuth, rotationController.updateAttendInfo); 22 | 23 | // 사서 일정 삭제 24 | router.delete("/update", isAuth, rotationController.deleteAttendInfo); 25 | 26 | export default router; -------------------------------------------------------------------------------- /src/routes/slack.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import * as slackController from "../controller/slack.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/", slackController.publishMessages); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /src/routes/timeline.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import * as timelineController from "../controller/timeline.controller.js"; 4 | import { isAuth } from "../middleware/auth.js"; 5 | import { fileSizeLimitErrorHandler, timelineUpload } from "../middleware/uploads.js"; 6 | 7 | const router = express.Router(); 8 | 9 | // 타임라인 사진 가져오기 10 | router.get("/", timelineController.listAllImages); 11 | 12 | // 타임라인 사진 업로드 13 | router.post( 14 | "/upload", 15 | isAuth, 16 | timelineUpload.any(), 17 | fileSizeLimitErrorHandler, 18 | timelineController.upload, 19 | ); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/routes/together.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { isAuth } from "../middleware/auth.js"; 4 | import * as togetherController from "../controller/together.controller.js"; 5 | 6 | const router = express.Router(); 7 | 8 | router.get("/", togetherController.getEventList); 9 | router.post("/", isAuth, togetherController.createEvent); 10 | router.post("/register", isAuth, togetherController.register); 11 | router.delete("/unregister/:id", isAuth, togetherController.unregister); 12 | router.post("/matching", isAuth, togetherController.matching); 13 | router.get("/matching", togetherController.getEventInfo); 14 | router.get("/matching/:id", togetherController.getTeam); 15 | router.get("/point", togetherController.getAttendingPoint); 16 | router.get("/:id", togetherController.getEvent); 17 | router.delete("/:id", isAuth, togetherController.deleteEvent); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /src/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import * as userController from "../controller/user.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | //GET 유저리스트 조회 8 | router.get("/userList", userController.getUserList); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /src/s3.js: -------------------------------------------------------------------------------- 1 | import aws from "aws-sdk"; 2 | import { config } from "./config.js"; 3 | 4 | export const s3 = new aws.S3({ 5 | accessKeyId: config.s3.access_key_id, 6 | secretAccessKey: config.s3.secret_access_key, 7 | bucket: config.s3.bucket, 8 | region: config.s3.region, 9 | }); 10 | -------------------------------------------------------------------------------- /src/swagger/swagger.js: -------------------------------------------------------------------------------- 1 | import swaggerAutogen from "swagger-autogen"; 2 | import dotEnv from "dotenv"; 3 | 4 | dotEnv.config(); 5 | 6 | const options = { 7 | info: { 8 | title: "Together42 web service API", 9 | version: "1.0.0", 10 | description: "Together42 web service, Express and documented with Swagger", 11 | }, 12 | host: process.env.BACKEND_LOCAL_HOST ?? process.env.BACKEND_TEST_HOST, 13 | contact: { 14 | name: "tkim", 15 | url: "https://github.com/kth2624", 16 | email: "dev.tkim42@gmail.com", 17 | }, 18 | schemes: [process.env.BACKEND_LOCAL_HOST ? "http" : "https"], 19 | securityDefinitions: { 20 | bearerAuth: { 21 | type: "apiKey", 22 | name: "Authorization", 23 | scheme: "bearer", 24 | in: "header", 25 | bearerFormat: "JWT", 26 | }, 27 | }, 28 | }; 29 | 30 | const outputFile = "./src/swagger/swagger-docs.json"; 31 | const endpointsFiles = ["../app.js"]; 32 | 33 | swaggerAutogen(outputFile, endpointsFiles, options); 34 | -------------------------------------------------------------------------------- /src/swagger/swagger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node src/swagger/swagger.js 4 | swagger_output='src/swagger/swagger-docs.json' 5 | 6 | IFS=$'\n' 7 | router=`cat $swagger_output | grep -n "/api/" | awk -F: '{print $1}'` 8 | router_array=($router) 9 | router_array_length=${#router_array[@]} 10 | 11 | http_methods="\"get\":\|\"post\":\|\"delete\":\|\"put\":\|\"patch\":" 12 | security="\"security\": [{\"bearerAuth\": []}]," 13 | 14 | for ((var=0 ; var < $router_array_length ; var++)); 15 | do 16 | api_tags=`sed -n "${router_array[$var]}p" $swagger_output | awk '{split($1, array, "/"); printf "\"tags\": [\"%s\"],\n", array[3];}'` 17 | next_var=`expr ${var} + 1` 18 | if [ $next_var -ge $router_array_length ]; then 19 | method_array=`sed -n "${router_array[$var]},\'$'p" $swagger_output | grep -n "${http_methods}" | awk -F: '{print $1}'` 20 | else 21 | method_array=`sed -n "${router_array[$var]},${router_array[$next_var]}p" $swagger_output | grep -n "${http_methods}" | awk -F: '{print $1}'` 22 | fi 23 | 24 | for line_num in ${method_array[@]}; 25 | do 26 | modify_line=`expr ${router_array[$var]} + ${line_num} - 1` 27 | sed -i -e "${modify_line}s/\$/${api_tags}${security}/i" $swagger_output 28 | done 29 | done 30 | 31 | sed -i -e 's/\"tags\":/\n "tags\":/g' $swagger_output 32 | sed -i -e 's/\"security\":/\n "security\":/g' $swagger_output 33 | [ -f $swagger_output-e ] && rm $swagger_output-e 34 | 35 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Date 형식의 today를 받아 3 | * YYYY-MM-DD 형식의 tomorrow를 반환 4 | */ 5 | export function getTomorrow(today) { 6 | const tomorrow = new Date(today.setDate(today.getDate() + 1)); 7 | const month = `${tomorrow.getMonth() + 1 < 10 ? "0" : ""}${ 8 | tomorrow.getMonth() + 1 9 | }`; 10 | const day = `${tomorrow.getDate() < 10 ? "0" : ""}${tomorrow.getDate()}`; 11 | const dateFormat = `${tomorrow.getFullYear()}-${month}-${day}`; 12 | return dateFormat; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/rotation.calendar.js: -------------------------------------------------------------------------------- 1 | import { addHolidayInfo, getHolidayByMonth } from "../data/rotation.js"; 2 | import request from "request"; 3 | import { config } from "../config.js"; 4 | 5 | export function getTodayDate() { 6 | const today = new Date(); 7 | const date = today.getDate(); 8 | return (date); 9 | } 10 | 11 | const MONTH_IN_YEAR = 12; 12 | const DAY_IN_WEEK = 7; 13 | 14 | const DAY_OF_THURSDAY = 4; 15 | 16 | const getFirstDateOfMonth = (date, offsetMonth = 0) => 17 | new Date(date.getFullYear(), date.getMonth() + offsetMonth, 1); 18 | const getFirstDayOfMonth = (date, offsetMonth = 0) => getFirstDateOfMonth(date, offsetMonth).getDay(); 19 | 20 | const getFourthWeekPeriod = (date = new Date()) => { 21 | const firstDay = getFirstDayOfMonth(date); // 첫째날 day 22 | let dateOfThursdayOnFirstWeek; // 첫쨰주에 무조건 존재하는 목요일을 기준으로 탐색. 23 | if (firstDay <= DAY_OF_THURSDAY) { 24 | dateOfThursdayOnFirstWeek = 1 + DAY_OF_THURSDAY - firstDay; 25 | } else { 26 | dateOfThursdayOnFirstWeek = 1 + DAY_IN_WEEK + DAY_OF_THURSDAY - firstDay; 27 | } 28 | const dateOfThursdayOnFourthWeek = dateOfThursdayOnFirstWeek + 3 * DAY_IN_WEEK; 29 | const dateOfMondayOnFourthWeek = dateOfThursdayOnFourthWeek - 3; 30 | const dateOfSundayOnFourthWeek = dateOfThursdayOnFourthWeek + 3; 31 | return [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek]; 32 | }; 33 | 34 | export const getFourthWeekdaysOfMonth = (date = new Date()) => { 35 | const [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek] = getFourthWeekPeriod(date); 36 | const dateOfFridayOnFourthWeek = dateOfSundayOnFourthWeek - 2; 37 | const fourthWeekDays = []; 38 | for (let i = 0; i < 5; i++) { 39 | const day = dateOfMondayOnFourthWeek + i; 40 | fourthWeekDays.push(day); 41 | } 42 | return (fourthWeekDays); 43 | }; 44 | 45 | export async function storeHolidayInfo() { 46 | const URL = 'http://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getHoliDeInfo'; 47 | let month = new Date().getMonth() + 2; 48 | const solMonth = month < 10 ? '0' + month : month; 49 | let year = new Date().getFullYear(); 50 | const solYear = month === 1 ? year += 1 : year; 51 | const SERVICEKEY = config.openApi.holidayKey; 52 | const requestUrl = URL + '?' + 'solYear=' + solYear + '&' + 'solMonth=' + solMonth + '&' + 'ServiceKey=' + SERVICEKEY + '&' + '_type=json'; 53 | 54 | return new Promise((resolve, reject) => { 55 | request(requestUrl, async function (error, response, body) { 56 | if (error) { 57 | reject ({ status: response.statusCode, message: "openApi query error" }); 58 | } 59 | const returnData = JSON.parse(body); 60 | const items = returnData['response']['body']['items']['item']; 61 | if (Array.isArray(items)) { 62 | for (let i = 0; i < items.length; i++) { 63 | let holidayArray = []; 64 | let item = items[i]; 65 | if (item['locdate']) { 66 | console.log(item['locdate'], typeof(item['locdate'])); 67 | const holidayInfo = { 68 | year: item['locdate'].toString().substr(0, 4), 69 | month: item['locdate'].toString().substr(4, 2), 70 | day: item['locdate'].toString().substr(6, 2), 71 | info: item['dateName'] 72 | }; 73 | holidayArray.push(holidayInfo); 74 | 75 | await addHolidayInfo(holidayInfo) 76 | .catch(error => reject({ status: 500, message: error })); 77 | } 78 | } 79 | } else if (typeof items === 'object') { 80 | if (items['locdate']) { 81 | let holidayArray = []; 82 | const holidayInfo = { 83 | year: items['locdate'].toString().substr(0, 4), 84 | month: items['locdate'].toString().substr(4, 2), 85 | day: items['locdate'].toString().substr(6, 2), 86 | info: items['dateName'] 87 | }; 88 | holidayArray.push(holidayInfo); 89 | 90 | await addHolidayInfo(holidayInfo) 91 | .catch(error => reject({ status: 500, message: error })); 92 | } 93 | } 94 | resolve ({ status: response.statusCode }); 95 | }); 96 | }); 97 | } 98 | 99 | export async function getHolidayOfMonth() { 100 | let month = new Date().getMonth() + 2; 101 | let year = month === 1 ? new Date().getFullYear() + 1 : new Date().getFullYear(); 102 | month = 11; 103 | const holidayArray = await getHolidayByMonth({ year: year, month: month }); 104 | console.log(holidayArray); 105 | } -------------------------------------------------------------------------------- /src/utils/rotation.together.js: -------------------------------------------------------------------------------- 1 | 2 | import { getHolidayByMonth } from "../data/rotation.js"; 3 | 4 | function sortByArray(array) { 5 | array.sort((a, b) => b.attendLimit.length - a.attendLimit.length); 6 | } 7 | 8 | function shuffle(array) { 9 | array.sort(() => Math.random() - 0.5); 10 | } 11 | 12 | function isEmptyObj(object) { 13 | return JSON.stringify(object) === "{}"; 14 | } 15 | 16 | async function isNotHoliday(day) { 17 | const month = (new Date().getMonth() + 1) % 12 + 1; 18 | const year = month === 1 ? new Date().getFullYear() + 1 : new Date().getFullYear(); 19 | let holidayInfo = []; 20 | holidayInfo['year'] = year; 21 | holidayInfo['month'] = month; 22 | const response = await getHolidayByMonth(holidayInfo); 23 | const holidayArray = response.map(item => parseInt(item.day)); 24 | if (holidayArray.indexOf(Number(day)) >= 0) { 25 | return false; 26 | } 27 | return true; 28 | } 29 | 30 | export async function initMonthArray() { 31 | let year = new Date().getFullYear(); 32 | const month = new Date().getMonth(); 33 | const nextMonth = (month + 1) % 12 + 1; 34 | if (nextMonth === 1) 35 | year += 1; 36 | const daysOfMonth = new Date(year, nextMonth, 0).getDate(); 37 | const tmpMonthArray = []; 38 | 39 | let tmpWeek = []; 40 | 41 | for (let i = 1; i <= daysOfMonth; i++) { 42 | let tmpDayObject = {}; 43 | let tmp = []; 44 | if (new Date(year, nextMonth - 1, i).getDay() > 0 && 45 | new Date(year, nextMonth - 1, i).getDay() < 6) { 46 | let day = new Date(year, nextMonth - 1, i).getDate(); 47 | if (await isNotHoliday(day)) { 48 | tmp.push(0); 49 | tmp.push(0); 50 | tmpDayObject = {day: day, arr: tmp}; 51 | if (i === daysOfMonth) 52 | tmpMonthArray.push(tmpWeek); 53 | } 54 | } 55 | else { 56 | tmpMonthArray.push(tmpWeek); 57 | tmpDayObject = {}; 58 | tmpWeek = []; 59 | } 60 | if (!isEmptyObj(tmpDayObject)) { 61 | tmpWeek.push(tmpDayObject); 62 | } 63 | } 64 | const result = tmpMonthArray.filter(arrlen => arrlen.length > 0); 65 | return ({monthArray: result, nextMonth: nextMonth, year: year}); 66 | } 67 | 68 | export function setRotation(attendance, monthArrayInfo) { 69 | let canDuplicate = false; 70 | if (attendance.length < 10) canDuplicate = true; 71 | let participation = 1; 72 | let participants = []; 73 | for (let i = 0; i < attendance.length; i++) { 74 | participants.push({id: attendance[i].id, intraId: attendance[i].intraId, 75 | attendLimit: JSON.parse(attendance[i].attendLimit), attend: 0}); 76 | } 77 | shuffle(participants); 78 | sortByArray(participants); 79 | let checkContinue = false; 80 | let continueIndex = 0; 81 | let isLooped = false; 82 | let isLoopedAgain = false; 83 | for (let i = 0; i < monthArrayInfo.monthArray.length; i++) { 84 | for (let j = 0; j < monthArrayInfo.monthArray[i].length; j++) { 85 | // console.log("================================"); 86 | let participant1 = undefined; 87 | let arrIndex = 0; 88 | for (let k = 0; k < participants.length; k++) { 89 | if (checkContinue === true) { 90 | k += continueIndex; 91 | } 92 | // console.log("first :", participants[k].intraId, participants[k].attend); 93 | if (participants[k].attend < participation) { 94 | if (participants[k].attendLimit 95 | .indexOf(monthArrayInfo.monthArray[i][j].day) === -1) { 96 | if (monthArrayInfo.monthArray[i][j].arr 97 | .indexOf(participants[k].intraId) === -1) { 98 | participant1 = participants[k]; 99 | participant1.attend += 1; 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | if (canDuplicate && participant1 === undefined) { 106 | for (let k = 0; k < participants.length; k++) { 107 | if (participants[k].attend < participation + 1) { 108 | // console.log("dup first :", participants[k].intraId, participants[k].attend); 109 | if (participants[k].attendLimit 110 | .indexOf(monthArrayInfo.monthArray[i][j].day) === -1) { 111 | if (monthArrayInfo.monthArray[i][j].arr 112 | .indexOf(participants[k].intraId) === -1) { 113 | participant1 = participants[k]; 114 | participant1.attend += 1; 115 | break; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | if (participant1 === undefined) { 122 | participation += 1; 123 | if (j > 0) { 124 | j -= 1; 125 | } else { 126 | j = -1; 127 | } 128 | if (isLooped === false) { 129 | isLooped = true; 130 | } else if (isLooped === true) { 131 | participation -= 1; 132 | isLooped = false; 133 | j += 1; 134 | } 135 | continue; 136 | } else { 137 | continueIndex = 0; 138 | checkContinue = false; 139 | isLooped = false; 140 | monthArrayInfo.monthArray[i][j].arr[arrIndex++] = participant1.intraId; 141 | } 142 | // console.log("first: ", monthArrayInfo.monthArray[i][j], participation); 143 | 144 | shuffle(participants); 145 | sortByArray(participants); 146 | // console.log("------------------"); 147 | 148 | // 한 번 더 반복 149 | let participant2 = undefined; 150 | for (let k = 0; k < participants.length; k++) { 151 | // console.log("second :", participants[k].intraId, participants[k].attend); 152 | if (participants[k].attend < participation) { 153 | if (participants[k].attendLimit 154 | .indexOf(monthArrayInfo.monthArray[i][j].day) === -1) { 155 | if (monthArrayInfo.monthArray[i][j].arr 156 | .indexOf(participants[k].intraId) === -1) { 157 | participant2 = participants[k]; 158 | participant2.attend += 1; 159 | break; 160 | } 161 | } 162 | } 163 | } 164 | if (canDuplicate && participant2 === undefined) { 165 | for (let k = 0; k < participants.length; k++) { 166 | if (participants[k].attend < participation + 1) { 167 | // console.log("dup second :", participants[k].intraId, participants[k].attend); 168 | if (participants[k].attendLimit 169 | .indexOf(monthArrayInfo.monthArray[i][j].day) === -1) { 170 | if (monthArrayInfo.monthArray[i][j].arr 171 | .indexOf(participants[k].intraId) === -1) { 172 | participant2 = participants[k]; 173 | participant2.attend += 1; 174 | break; 175 | } 176 | } 177 | } 178 | } 179 | } 180 | if (participant2 === undefined) { 181 | participation += 1; 182 | if (j > 0) { 183 | j -= 1; 184 | } else { 185 | j = -1; 186 | } 187 | if (participant1 && isLooped === false) { 188 | if (isLoopedAgain === true) { 189 | participation -= 1; 190 | isLoopedAgain = false; 191 | continue; 192 | } 193 | let index = participants.findIndex(obj => obj.intraId === participant1.intraId); 194 | continueIndex = index; 195 | checkContinue = true; 196 | isLooped = true; 197 | isLoopedAgain = true; 198 | participant1.attend -= 1; 199 | monthArrayInfo.monthArray[i][j + 1].arr[0] = 0; 200 | } else if (isLooped === true) { 201 | participation -= 1; 202 | isLooped = false; 203 | isLoopedAgain = false; 204 | j += 1; 205 | } 206 | continue; 207 | } else { 208 | continueIndex = 0; 209 | checkContinue = false; 210 | isLooped = false; 211 | isLoopedAgain = false; 212 | monthArrayInfo.monthArray[i][j].arr[arrIndex--] = participant2.intraId; 213 | } 214 | // console.log("second: ", monthArrayInfo.monthArray[i][j], participation); 215 | // console.log("================================"); 216 | } 217 | } 218 | // for (let i = 0; i < monthArrayInfo.monthArray.length; i++) { 219 | // for (let j = 0; j < monthArrayInfo.monthArray[i].length; j++) { 220 | // console.log(monthArrayInfo.monthArray[i][j]); 221 | // } 222 | // } 223 | return ({monthArray: monthArrayInfo, participants: participants}); 224 | } 225 | 226 | export function checkAttend(attendInfo) { 227 | let loop = 1; 228 | let flag = true; 229 | while (loop) { 230 | flag = true; 231 | if (loop > 100) { 232 | break; 233 | } 234 | for (let i = 0; i < attendInfo.participants.length; i++) { 235 | if (attendInfo.participants[i].attend === 0) flag = false; 236 | } 237 | if (flag === true) 238 | break; 239 | loop++; 240 | } 241 | if (flag === false) 242 | return ({status: flag, loop: loop, message: "적합한 매칭을 만들지 못했습니다."}); 243 | else 244 | return ({status: flag, loop: loop, ...attendInfo}); 245 | } 246 | 247 | // import * as rotationRepository from "../data/rotation.js"; 248 | 249 | // let monthArrayInfo = initMonthArray(); 250 | // let participants = await rotationRepository.getParticipants({ month: 3, year: 2023 }); 251 | // let attendResult = checkAttend(setRotation(participants, monthArrayInfo)).participants; 252 | 253 | // for (let i = 0; i < attendResult.length; i++) { 254 | // console.log(attendResult[i].intraId, attendResult[i].attend); 255 | // } -------------------------------------------------------------------------------- /src/utils/slack.service.js: -------------------------------------------------------------------------------- 1 | import { config } from "../config.js"; 2 | import { publishMessage } from "../controller/slack.controller.js"; 3 | import { getMonthlyLibarians, getLibariansByDate } from "../data/rotation.js"; 4 | import { getTomorrow } from "./date.js"; 5 | 6 | export async function postSlackTomorrowLibrarians() { 7 | const tomorrow = getTomorrow(new Date()); 8 | const libarians = await getLibariansByDate(tomorrow); 9 | 10 | const messages = libarians.map((libarian) => { 11 | const { intraId, slackId } = libarian; 12 | let channel = slackId; 13 | if (process.env.BACKEND_LOCAL_HOST || process.env.BACKEND_TEST_HOST) { 14 | channel = config.slack.jip; 15 | } 16 | return publishMessage( 17 | channel, 18 | `[리마인드] ${intraId}님은 내일(${tomorrow}) 사서입니다!`, 19 | ); 20 | }); 21 | await Promise.all(messages); 22 | } 23 | 24 | export async function postSlackMonthlyLibrarian(year, month) { 25 | console.log(year, month); 26 | const libarians = await getMonthlyLibarians(year, month); 27 | console.log(libarians); 28 | 29 | const messages = libarians.map((libarian) => { 30 | const { intraId, slackId } = libarian; 31 | let channel = slackId; 32 | const attendDates = libarian.attendDate 33 | .split(",") 34 | .filter((date) => date.length > 0) 35 | .map((date) => "`" + date + "`") 36 | .join(", "); 37 | if (process.env.BACKEND_LOCAL_HOST || process.env.BACKEND_TEST_HOST) { 38 | channel = config.slack.jip; 39 | } 40 | return publishMessage(channel, `${intraId}님은 ${attendDates} 사서입니다.`); 41 | }); 42 | await Promise.all(messages); 43 | } 44 | --------------------------------------------------------------------------------