├── .all-contributorsrc ├── .github └── ISSUE_TEMPLATE │ ├── ---------.md │ └── ------.md ├── .vscode └── settings.json ├── README.md └── WeathyServer ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── app.js ├── bin └── www ├── controllers ├── authController.js ├── calendarController.js ├── clothesController.js ├── userController.js ├── weatherController.js └── weathyController.js ├── models ├── category.js ├── climate.js ├── climateMessage.js ├── clothes.js ├── dailyWeather.js ├── hourlyWeather.js ├── index.js ├── location.js ├── token.js ├── user.js ├── weathy.js └── weathyClothes.js ├── modules ├── exception.js ├── logger.js ├── statusCode.js ├── swagger.js ├── tokenMiddleware.js └── uploadFile.js ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes ├── auth.js ├── index.js ├── users.js ├── weather.js └── weathy.js ├── services ├── calendarService.js ├── climateService.js ├── clothesService.js ├── index.js ├── locationService.js ├── tokenService.js ├── userService.js ├── weatherService.js └── weathyService.js ├── tests ├── modules │ ├── logger.spec.js │ └── tokenMiddleware.spec.js ├── services │ ├── calendarService.spec.js │ ├── climateService.spec.js │ ├── clothesService.spec.js │ ├── locationService.spec.js │ ├── tokenService.spec.js │ ├── userService.spec.js │ ├── weatherService.spec.js │ └── weathyService.spec.js └── utils │ ├── dateUtils.spec.js │ └── tokenUtils.spec.js ├── utils ├── dateUtils.js └── tokenUtils.js └── views ├── error.jade ├── index.jade └── layout.jade /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "yxxshin", 10 | "name": "Yeon Sang Shin", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/63148508?v=4", 12 | "profile": "https://github.com/yxxshin", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "seonuk", 19 | "name": "seonuk", 20 | "avatar_url": "https://avatars3.githubusercontent.com/u/22928068?v=4", 21 | "profile": "https://github.com/seonuk", 22 | "contributions": [ 23 | "code" 24 | ] 25 | },{ 26 | "login": "dshyun0226", 27 | "name": "Jahyun Kim", 28 | "avatar_url": "https://avatars3.githubusercontent.com/u/8098698?v=4", 29 | "profile": "https://github.com/dshyun0226", 30 | "contributions": [ 31 | "code" 32 | ] 33 | } 34 | ], 35 | "contributorsPerLine": 7, 36 | "projectName": "WeathyServer", 37 | "projectOwner": "TeamWeathy", 38 | "repoType": "github", 39 | "repoHost": "https://github.com", 40 | "skipCi": true 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 새로운 기능 개발 3 | about: 신규 혹은 추가 기능을 개발합니다. 4 | title: '' 5 | labels: IMPLEMENTATION 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 제목 11 | 12 | - 신규 기능의 개발 배경 및 효과에 대해서 설명해주세요. 13 | - e.g., 로그인 API를 신규 개발 14 | - e.g., 참고: [로그인 API 명세](link) 15 | 16 | ## 작업 브랜치 17 | - `master` <- `dev` 18 | 19 | ## 확인 사항 20 | - [ ] PR 생성 전 확인이 필요한 작업들을 작성해주세요. 21 | - [ ] e.g., [로그인 API 명세](link)의 "개발 여부" 업데이트 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 버그 리포트 3 | about: 올바르게 작동하지 않는 기능을 제보 및 수정합니다. 4 | title: '' 5 | labels: BUG 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 제목 11 | 12 | ## 개요 13 | ### 발생 환경 14 | - 버그가 발생한 환경에 대해서 자세히 설명해주세요. 15 | - e.g., macOS Catalina, Postman 7.36.0 16 | 17 | ### 기대 내용 18 | - 재현 방법을 최대한 자세히 설명해주세요. 19 | - e.g., 헤더가 "Accept: Application/json" 를 포함합니다. 20 | - 정상적으로 작동했을 때의 상황을 작성해주세요. 21 | - e.g., /users/3으로 GET 요청을 보냈을 때, 반환되는 사용자의 정보에 id가 3인 사용자의 이름이 포함되어야 합니다. 22 | 23 | ### 현재 내용 24 | - 현재 상황을 작성해주세요. 25 | - e.g., 500에러가 발생합니다. 26 | 27 | ## 레퍼런스 28 | - 버그가 발생했을 때의 스크린샷, 로그 등을 첨부해주세요. 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Set the default 3 | "editor.formatOnSave": false, 4 | // Enable per-language 5 | "[javascript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "editor.codeActionsOnSave": { 9 | // For ESLint 10 | "source.fixAll.eslint": true 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeathyServer 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-blue.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | ## 나에게 돌아오는 맞춤 서비스, Weathy 🌤 8 | 9 | 10 | ## Contributors ✨ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

Yeon Sang Shin

💻

seonuk

💻

Jahyun Kim

💻
22 | 23 | 24 | 25 | 26 | 27 | 28 | ### dependencies module (package.json) 29 | #### Dev module: 30 | ```json 31 | "devDependencies": { 32 | "decache": "^4.6.0", 33 | "eslint": "^7.16.0", 34 | "eslint-config-prettier": "^7.1.0", 35 | "eslint-plugin-prettier": "^3.3.0", 36 | "mocha": "^8.2.1", 37 | "prettier": "2.2.1", 38 | "swagger-jsdoc": "^6.0.0", 39 | "swagger-ui-express": "^4.1.6" 40 | } 41 | ``` 42 | 43 | #### module: 44 | ```json 45 | "dependencies": { 46 | "app-root-path": "^3.0.0", 47 | "cookie-parser": "~1.4.4", 48 | "crypto-random-string": "^3.3.0", 49 | "dayjs": "^1.10.2", 50 | "debug": "~2.6.9", 51 | "express": "~4.16.1", 52 | "http-errors": "~1.6.3", 53 | "jade": "~1.11.0", 54 | "morgan": "~1.9.1", 55 | "mysql2": "^2.2.5", 56 | "request": "^2.88.2", 57 | "request-promise": "^4.2.6", 58 | "sequelize": "^6.3.5", 59 | "sequelize-cli": "^6.2.0", 60 | "winston": "^3.3.3", 61 | "winston-daily-rotate-file": "^4.5.0" 62 | } 63 | ``` 64 | 65 | ### ER Diagram 66 | 67 | 68 | 69 | ### 서버 아키텍쳐 70 | 71 | 72 | 73 | ### 핵심 기능 설명 74 | open weather api를 사용해서 날씨를 수집하고 해당 날씨에 대해 사용자가 자신의 옷차림과 상태를 기록한다. 75 | 기록된 데이터를 바탕으로 오늘 날씨와 비슷한 날씨의 기록 데이터를 가져와 날씨 판단에 있어 비교척도를 제공해준다. 76 | 77 | ### 팀별 역할 분담 78 | - 신연상 : API 위키 문서 관리, Login, User, Clothes API 개발 및 테스트코드 작성 79 | - 최선욱 : Open Weather Batch 프로그램 개발, Weathy API 개발 및 테스트코드 작성 80 | - 김자현 : 스키마 설계, DB 권한 및 계정 , Calendar, Weather API 개발 및 테스트코드 작성 81 | 이외의 API 설계, 코드 리뷰 등은 함께 하였음! 82 | 83 | ### API 명세서 84 | [API 명세서 링크](https://github.com/TeamWeathy/WeathyServer/wiki) 85 | -------------------------------------------------------------------------------- /WeathyServer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | // 코드 포맷을 prettier로 설정 3 | "plugins": [ 4 | "prettier" 5 | ], 6 | "env": { 7 | // "express":true, 8 | "node": true, 9 | "jest": true, 10 | "mocha": true, 11 | "browser": false, 12 | "commonjs": true, 13 | "es2021": true 14 | }, 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:prettier/recommended" 18 | ], 19 | "parserOptions": { 20 | "ecmaVersion": 12, 21 | "sourceType": "script" 22 | }, 23 | "ignorePatterns": [ 24 | "node_modules/" 25 | ], 26 | // ESLint 룰을 설정 27 | "rules": { 28 | // prettier에 맞게 룰을 설정 29 | "prettier/prettier": "error" 30 | }, 31 | "globals": { 32 | "process": false, 33 | "__dirname": false 34 | } 35 | } -------------------------------------------------------------------------------- /WeathyServer/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,node,vscode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,node,vscode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Node ### 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | lerna-debug.log* 41 | 42 | # Diagnostic reports (https://nodejs.org/api/report.html) 43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # Directory for instrumented libs generated by jscoverage/JSCover 52 | lib-cov 53 | 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (https://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # TypeScript v1 declaration files 78 | typings/ 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Microbundle cache 90 | .rpt2_cache/ 91 | .rts2_cache_cjs/ 92 | .rts2_cache_es/ 93 | .rts2_cache_umd/ 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn Integrity file 102 | .yarn-integrity 103 | 104 | # dotenv environment variables file 105 | .env 106 | .env.test 107 | .env*.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | .cache 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | .next 115 | 116 | # Nuxt.js build / generate output 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | .cache/ 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | # https://nextjs.org/blog/next-9-1#public-directory-support 124 | # public 125 | 126 | # vuepress build output 127 | .vuepress/dist 128 | 129 | # Serverless directories 130 | .serverless/ 131 | 132 | # FuseBox cache 133 | .fusebox/ 134 | 135 | # DynamoDB Local files 136 | .dynamodb/ 137 | 138 | # TernJS port file 139 | .tern-port 140 | 141 | # Stores VSCode versions used for testing VSCode extensions 142 | .vscode-test 143 | 144 | ### vscode ### 145 | .vscode/* 146 | 147 | !.vscode/tasks.json 148 | !.vscode/launch.json 149 | !.vscode/extensions.json 150 | *.code-workspace 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/macos,node,vscode 153 | 154 | config/ -------------------------------------------------------------------------------- /WeathyServer/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /WeathyServer/app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const cookieParser = require('cookie-parser'); 5 | const morgan = require('morgan'); 6 | const sc = require('./modules/statusCode'); 7 | 8 | const indexRouter = require('./routes/index'); 9 | const authRouter = require('./routes/auth'); 10 | const usersRouter = require('./routes/users'); 11 | const weathyRouter = require('./routes/weathy'); 12 | const weatherRouter = require('./routes/weather'); 13 | 14 | const { swaggerUi, specs } = require('./modules/swagger'); 15 | const exception = require('./modules/exception'); 16 | const logger = require('winston'); 17 | 18 | const app = express(); 19 | 20 | // set etag false 21 | // 동적 요청에 대한 응답을 보낼 때 etag 생성을 하지 않도록 설정 22 | app.set("etag", false); 23 | 24 | // 정적 요청에 대한 응답을 보낼 때 etag 생성을 하지 않도록 설정 25 | const options = { etag: false }; 26 | app.use(express.static("public", options)); 27 | 28 | // view engine setup 29 | app.set('views', path.join(__dirname, 'views')); 30 | app.set('view engine', 'jade'); 31 | 32 | const db = require('./models/index.js'); 33 | 34 | 35 | app.use(morgan('dev')); 36 | app.use(express.json()); 37 | app.use(express.urlencoded({ extended: false })); 38 | app.use(cookieParser()); 39 | app.use(express.static(path.join(__dirname, 'public'))); 40 | 41 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); 42 | app.use('/', indexRouter); 43 | app.use('/auth', authRouter); 44 | app.use('/users', usersRouter); 45 | app.use('/weather', weatherRouter); 46 | app.use('/weathy', weathyRouter); 47 | 48 | // catch 404 and forward to error handler 49 | app.use(function (req, res, next) { 50 | next(createError(404)); 51 | }); 52 | 53 | // error handler 54 | app.use(function (err, req, res, next) { 55 | res.status(err.status || 500).json({ 56 | message: err.message 57 | }); 58 | }); 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /WeathyServer/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('weathyserver:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 62 | 63 | // handle specific listen errors with friendly messages 64 | switch (error.code) { 65 | case 'EACCES': 66 | console.error(bind + ' requires elevated privileges'); 67 | process.exit(1); 68 | break; 69 | case 'EADDRINUSE': 70 | console.error(bind + ' is already in use'); 71 | process.exit(1); 72 | break; 73 | default: 74 | throw error; 75 | } 76 | } 77 | 78 | /** 79 | * Event listener for HTTP server "listening" event. 80 | */ 81 | 82 | function onListening() { 83 | var addr = server.address(); 84 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 85 | debug('Listening on ' + bind); 86 | } 87 | -------------------------------------------------------------------------------- /WeathyServer/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const { Token } = require('../models'); 3 | const exception = require('../modules/exception'); 4 | const statusCode = require('../modules/statusCode'); 5 | const { userService } = require('../services'); 6 | 7 | module.exports = { 8 | login: async (req, res, next) => { 9 | const { uuid } = req.body; 10 | 11 | if (!uuid) { 12 | next(createError(400)); 13 | } 14 | try { 15 | const user = await userService.getUserByAccount(uuid); 16 | const userToken = await Token.findOne({ 17 | where: { user_id: user.id } 18 | }); 19 | const token = userToken.token; 20 | res.locals.tokenValue = token; 21 | res.status(statusCode.OK).json({ 22 | user: { 23 | id: user.id, 24 | nickname: user.nickname 25 | }, 26 | token: token, 27 | message: '로그인 성공' 28 | }); 29 | next(); 30 | } catch (error) { 31 | switch (error.message) { 32 | case exception.NO_USER: 33 | next(createError(401)); 34 | break; 35 | default: 36 | next(createError(500)); 37 | } 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /WeathyServer/controllers/calendarController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const statusCode = require('../modules/statusCode'); 3 | const dateUtils = require('../utils/dateUtils'); 4 | const { calendarService } = require('../services'); 5 | 6 | module.exports = { 7 | getCalendarOverviews: async (req, res, next) => { 8 | const { userId } = req.params; 9 | const { start, end } = req.query; 10 | 11 | if (!start || !end) { 12 | return next(createError(400)); 13 | } 14 | 15 | try { 16 | const validCalendarOverviewList = await calendarService.getValidCalendarOverviewList( 17 | userId, 18 | start, 19 | end 20 | ); 21 | let calendarOverviewList = []; 22 | let curDay = new Date(start); 23 | let endDay = new Date(end); 24 | let pos = 0; 25 | while (curDay <= endDay) { 26 | if ( 27 | pos < validCalendarOverviewList.length && 28 | validCalendarOverviewList[pos].date == 29 | dateUtils.formatDate(curDay) 30 | ) { 31 | calendarOverviewList.push(validCalendarOverviewList[pos++]); 32 | } else { 33 | calendarOverviewList.push(null); 34 | } 35 | curDay.setDate(curDay.getDate() + 1); 36 | } 37 | res.status(statusCode.OK).json({ 38 | calendarOverviewList, 39 | message: '캘린더 월 정보 조회 성공' 40 | }); 41 | next(); 42 | } catch (error) { 43 | switch (error.message) { 44 | default: 45 | return next(createError(500)); 46 | } 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /WeathyServer/controllers/clothesController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const exception = require('../modules/exception'); 3 | const statusCode = require('../modules/statusCode'); 4 | const { clothesService } = require('../services'); 5 | const { unionTwoCloset } = require('../services/clothesService'); 6 | 7 | module.exports = { 8 | getClothes: async (req, res, next) => { 9 | const { userId } = req.params; 10 | const { weathy_id: weathyId } = req.query; 11 | 12 | if (!userId) { 13 | next(createError(400)); 14 | } 15 | 16 | try { 17 | let closet = await clothesService.getClothesByUserId(userId); 18 | 19 | if (weathyId) { 20 | const weathyCloset = await clothesService.getClothesByWeathyId( 21 | userId, 22 | weathyId 23 | ); 24 | closet = unionTwoCloset(closet, weathyCloset); 25 | } 26 | res.status(statusCode.OK).json({ 27 | closet: closet, 28 | message: '옷 정보 조회 성공' 29 | }); 30 | next(); 31 | } catch (error) { 32 | switch (error.message) { 33 | default: 34 | next(createError(500)); 35 | } 36 | } 37 | }, 38 | addClothes: async (req, res, next) => { 39 | const { userId } = req.params; 40 | const { weathy_id: weathyId } = req.query; 41 | const { category, name } = req.body; 42 | 43 | if (!userId || !category || !name) { 44 | next(createError(400)); 45 | } 46 | 47 | try { 48 | await clothesService.addClothesByUserId(userId, category, name); 49 | let closet = await clothesService.getClothesByUserId(userId); 50 | if (weathyId) { 51 | const weathyCloset = await clothesService.getClothesByWeathyId( 52 | userId, 53 | weathyId 54 | ); 55 | closet = unionTwoCloset(closet, weathyCloset); 56 | } 57 | 58 | res.status(statusCode.OK).json({ 59 | closet, 60 | message: '옷 추가 성공' 61 | }); 62 | next(); 63 | } catch (error) { 64 | console.log(error.message); 65 | switch (error.message) { 66 | case exception.ALREADY_CLOTHES: 67 | next(createError(403)); 68 | break; 69 | default: 70 | next(createError(500)); 71 | } 72 | } 73 | }, 74 | deleteClothes: async (req, res, next) => { 75 | const { userId } = req.params; 76 | const { clothes } = req.body; 77 | 78 | if (!userId || !clothes) { 79 | next(createError(400)); 80 | } 81 | 82 | try { 83 | const closet = await clothesService.deleteClothesByUserId( 84 | userId, 85 | clothes 86 | ); 87 | 88 | res.status(statusCode.OK).json({ 89 | closet: closet, 90 | message: '옷 삭제 성공' 91 | }); 92 | next(); 93 | } catch (error) { 94 | console.log(error.message); 95 | switch (error.message) { 96 | case exception.NO_CLOTHES: 97 | next(createError(400)); 98 | break; 99 | case exception.NOT_AUTHORIZED_CLOTHES: 100 | next(createError(403)); 101 | break; 102 | default: 103 | console.log(error.message); 104 | next(createError(500)); 105 | } 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /WeathyServer/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const exception = require('../modules/exception'); 3 | const statusCode = require('../modules/statusCode'); 4 | const { userService, tokenService } = require('../services'); 5 | 6 | module.exports = { 7 | createUser: async (req, res, next) => { 8 | const { uuid, nickname } = req.body; 9 | 10 | if (!uuid || !nickname) { 11 | next(createError(400)); 12 | } 13 | 14 | try { 15 | const user = await userService.createUserByUuid(uuid, nickname); 16 | const token = await tokenService.createTokenOfUser(user.id); 17 | 18 | return res.status(statusCode.OK).json({ 19 | user: { 20 | id: user.id, 21 | nickname: user.nickname 22 | }, 23 | token: token, 24 | message: '유저 생성 성공' 25 | }); 26 | } catch (error) { 27 | switch (error.message) { 28 | case exception.ALREADY_USER: 29 | next(createError(400)); 30 | break; 31 | default: 32 | next(createError(500)); 33 | } 34 | } 35 | }, 36 | modifyUser: async (req, res, next) => { 37 | const token = res.locals.tokenValue; 38 | const { userId } = req.params; 39 | const { nickname } = req.body; 40 | 41 | if (!userId || !nickname) { 42 | next(createError(400)); 43 | } 44 | 45 | try { 46 | const user = await userService.modifyUserById(userId, nickname); 47 | 48 | res.status(statusCode.OK).json({ 49 | user: { 50 | id: user.id, 51 | nickname: user.nickname 52 | }, 53 | token: token, 54 | message: '유저 닉네임 변경 성공' 55 | }); 56 | next(); 57 | } catch (error) { 58 | switch (error.message) { 59 | case exception.INVALID_TOKEN: 60 | case exception.EXPIRED_TOKEN: 61 | case exception.MISMATCH_TOKEN: 62 | next(createError(401)); 63 | break; 64 | default: 65 | next(createError(500)); 66 | } 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /WeathyServer/controllers/weatherController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const exception = require('../modules/exception'); 3 | const statusCode = require('../modules/statusCode'); 4 | const dateUtils = require('../utils/dateUtils'); 5 | const { locationService, weatherService } = require('../services'); 6 | 7 | module.exports = { 8 | getWeatherByLocation: async (req, res, next) => { 9 | const { lat, lon } = req.query; 10 | let { code, date } = req.query; 11 | 12 | if (!date) { 13 | return next(createError(400)); 14 | } else if (!code && (!lat || !lon)) { 15 | return next(createError(400)); 16 | } else if (!code) { 17 | try { 18 | code = await locationService.getCode(lat, lon); 19 | } catch (error) { 20 | switch (error.message) { 21 | case exception.INVALID_LOCATION: 22 | return next(createError(204)); 23 | default: 24 | return next(createError(500)); 25 | } 26 | } 27 | } 28 | 29 | let time = date.split('T')[1] ? date.split('T')[1] : null; 30 | date = date.split('T')[0]; 31 | if (!date) { 32 | return next(createError(400)); 33 | } 34 | 35 | try { 36 | const overviewWeather = await weatherService.getOverviewWeather( 37 | code, 38 | date, 39 | time, 40 | dateUtils.format12 41 | ); 42 | if (!overviewWeather) { 43 | throw Error(exception.NO_DATA); 44 | } 45 | return res.status(statusCode.OK).json({ 46 | overviewWeather, 47 | message: '실시간 날씨 정보 반환 성공' 48 | }); 49 | } catch (error) { 50 | switch (error.message) { 51 | case exception.NO_DATA: 52 | return next(createError(204)); 53 | default: 54 | return next(createError(500)); 55 | } 56 | } 57 | }, 58 | getHourlyWeatherForecast: async (req, res, next) => { 59 | let { code, date } = req.query; 60 | if (!code || !date) { 61 | return next(createError(400)); 62 | } 63 | 64 | let time = date.split('T')[1]; 65 | date = date.split('T')[0]; 66 | if (!date || !time) { 67 | return next(createError(400)); 68 | } 69 | 70 | let hourlyWeatherList = []; 71 | for (let i = 0; i < 24; ++i) { 72 | hourlyWeatherList.push( 73 | await weatherService.getHourlyWeather( 74 | code, 75 | date, 76 | time, 77 | dateUtils.format24 78 | ) 79 | ); 80 | const { next_date, next_time } = dateUtils.getNextHour(date, time); 81 | date = next_date; 82 | time = next_time; 83 | } 84 | return res.status(statusCode.OK).json({ 85 | hourlyWeatherList, 86 | message: '시간 별 날씨 조회 성공' 87 | }); 88 | }, 89 | getDailyWeatherForecast: async (req, res, next) => { 90 | let { code, date } = req.query; 91 | if (!code || !date) { 92 | return next(createError(400)); 93 | } 94 | 95 | date = date.split('T')[0]; 96 | if (!date) { 97 | return next(createError(400)); 98 | } 99 | 100 | let dailyWeatherList = []; 101 | for (let i = 0; i < 7; ++i) { 102 | dailyWeatherList.push( 103 | await weatherService.getDailyWeatherWithClimateIconId( 104 | code, 105 | date 106 | ) 107 | ); 108 | date = dateUtils.getNextDay(date); 109 | } 110 | return res.status(statusCode.OK).json({ 111 | dailyWeatherList, 112 | message: '일자 별 날씨 조회 성공' 113 | }); 114 | }, 115 | getExtraDailyWeather: async (req, res, next) => { 116 | let { code, date } = req.query; 117 | if (!code || !date) { 118 | return next(createError(400)); 119 | } 120 | 121 | date = date.split('T')[0]; 122 | if (!date) { 123 | return next(createError(400)); 124 | } 125 | 126 | try { 127 | let extraWeather = await weatherService.getExtraDailyWeather( 128 | code, 129 | date 130 | ); 131 | return res.status(statusCode.OK).json({ 132 | extraWeather, 133 | message: '상세 날씨 조회 성공' 134 | }); 135 | } catch (error) { 136 | switch (error.message) { 137 | case exception.NO_DATA: 138 | return res.status(statusCode.NO_CONTENT).json({ 139 | extraWeather: null, 140 | message: '날씨 정보를 찾을 수 없음' 141 | }); 142 | default: 143 | return next(createError(400)); 144 | } 145 | } 146 | }, 147 | getWeathersByKeyword: async (req, res, next) => { 148 | let { keyword, date } = req.query; 149 | if (!keyword || !date) { 150 | return next(createError(400)); 151 | } 152 | 153 | let time = date.split('T')[1] || null; 154 | date = date.split('T')[0]; 155 | 156 | if (!date) { 157 | return next(createError(400)); 158 | } 159 | 160 | try { 161 | const overviewWeatherList = await weatherService.getOverviewWeathers( 162 | keyword, 163 | date, 164 | time, 165 | dateUtils.format12 166 | ); 167 | 168 | return res.status(statusCode.OK).json({ 169 | overviewWeatherList, 170 | message: '검색 성공' 171 | }); 172 | } catch (error) { 173 | switch (error.message) { 174 | case exception.NO_DATA: 175 | return next(createError(204)); 176 | default: 177 | return next(createError(500)); 178 | } 179 | } 180 | } 181 | }; 182 | -------------------------------------------------------------------------------- /WeathyServer/controllers/weathyController.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const sc = require('../modules/statusCode'); 3 | const weathyService = require('../services/weathyService'); 4 | const dayjs = require('dayjs'); 5 | 6 | const weatherService = require('../services/weatherService'); 7 | const exception = require('../modules/exception'); 8 | const logger = require('winston'); 9 | 10 | const { uploadS3 } = require('../modules/uploadFile'); 11 | 12 | module.exports = { 13 | getRecommendedWeathy: async (req, res, next) => { 14 | const { code, date } = req.query; 15 | const { userId } = req.params; 16 | const dateRegex = /^(19|20)\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/; 17 | 18 | const isExistWeather = await weatherService.getDailyWeather(code, date); 19 | 20 | if (!dateRegex.test(date) || !code || !isExistWeather) { 21 | return next( 22 | createError( 23 | sc.BAD_REQUEST, 24 | 'Parameter Error: 존재하지않는 날씨 데이터이거나 date, code 형식이 일치하지않습니다.' 25 | ) 26 | ); 27 | } 28 | 29 | try { 30 | const recommendedWeathy = await weathyService.getRecommendedWeathy( 31 | code, 32 | date, 33 | userId 34 | ); 35 | 36 | if (!recommendedWeathy) { 37 | res.status(sc.NO_CONTENTS).json({}); 38 | next(); 39 | } else { 40 | res.status(sc.OK).json({ 41 | ...recommendedWeathy, 42 | message: '추천 웨디 조회 성공' 43 | }); 44 | next(); 45 | } 46 | } catch (error) { 47 | switch (error.message) { 48 | default: 49 | return next(createError(sc.INTERNAL_SERVER_ERROR)); 50 | } 51 | } 52 | }, 53 | 54 | getWeathy: async (req, res, next) => { 55 | const { date } = req.query; 56 | const userId = res.locals.userId; 57 | const dateRegex = /^(19|20)\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/; 58 | const isPast = dayjs().isAfter(dayjs(date)); 59 | 60 | if (!dateRegex.test(date) || !isPast) { 61 | return next( 62 | createError( 63 | sc.BAD_REQUEST, 64 | 'Parameter Error: date 형식에 맞는 과거 날짜를 선택해주세요' 65 | ) 66 | ); 67 | } 68 | 69 | try { 70 | const weathy = await weathyService.getWeathy(date, userId); 71 | 72 | if (!weathy) { 73 | res.status(sc.NO_CONTENTS).json({}); 74 | next(); 75 | } else { 76 | res.status(sc.OK).json({ 77 | ...weathy, 78 | message: '웨디 기록 조회 성공' 79 | }); 80 | next(); 81 | } 82 | } catch (error) { 83 | switch (error.message) { 84 | default: 85 | next(createError(sc.INTERNAL_SERVER_ERROR)); 86 | } 87 | } 88 | }, 89 | 90 | createWeathy: async (req, res, next) => { 91 | try { 92 | if (!req.body.weathy) throw Error(exception.BAD_REQUEST); 93 | 94 | const weathyParams = JSON.parse(req.body.weathy); 95 | const { 96 | date, 97 | code, 98 | clothes, 99 | stampId, 100 | feedback, 101 | userId 102 | } = weathyParams; 103 | const dateRegex = /^(19|20)\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/; 104 | const isExistFile = req.file ? true : false; 105 | const resUserId = res.locals.userId; 106 | let imgUrl; 107 | 108 | if (!dateRegex.test(date) || !code || !clothes || !stampId) 109 | throw Error(exception.BAD_REQUEST); 110 | 111 | if (resUserId !== userId) { 112 | return next( 113 | createError( 114 | sc.INVALID_ACCOUNT, 115 | 'Token userId and body userId mismatch' 116 | ) 117 | ); 118 | } 119 | 120 | const dailyWeatherId = await weatherService.getDailyWeatherId( 121 | code, 122 | date 123 | ); 124 | 125 | if (!dailyWeatherId) { 126 | throw Error(exception.NO_DAILY_WEATHER); 127 | } 128 | 129 | const checkOwnerClothes = await weathyService.checkOwnerClothes( 130 | clothes, 131 | userId 132 | ); 133 | 134 | if (!checkOwnerClothes) { 135 | return next( 136 | createError( 137 | sc.NO_AUTHORITY, 138 | '옷에 대한 접근 권한이 없습니다.' 139 | ) 140 | ); 141 | } 142 | 143 | if (await weathyService.isDuplicateWeathy(userId, dailyWeatherId)) 144 | throw Error(exception.DUPLICATION_WEATHY); 145 | 146 | if (isExistFile) imgUrl = await uploadS3(userId, req.file.buffer); 147 | 148 | const weathyId = await weathyService.createWeathy( 149 | dailyWeatherId, 150 | clothes, 151 | stampId, 152 | userId, 153 | feedback || null, 154 | imgUrl || null 155 | ); 156 | 157 | res.status(sc.OK).json({ 158 | message: '웨디 기록 성공', 159 | weathyId 160 | }); 161 | next(); 162 | } catch (error) { 163 | logger.error(error.stack); 164 | 165 | switch (error.message) { 166 | case exception.NO_DAILY_WEATHER: 167 | return next( 168 | createError( 169 | sc.BAD_REQUEST, 170 | '해당 위치의 Daily Weather가 존재하지않음' 171 | ) 172 | ); 173 | case exception.DUPLICATION_WEATHY: 174 | return next( 175 | createError( 176 | sc.BAD_REQUEST, 177 | '잘못된 날짜에 Weathy 작성(중복된 웨디 작성)' 178 | ) 179 | ); 180 | case exception.NO_AUTHORITY: 181 | return next( 182 | createError( 183 | sc.BAD_REQUEST, 184 | 'Autority Error: Clothes 권한 없음' 185 | ) 186 | ); 187 | case exception.BAD_REQUEST: 188 | return next(createError(sc.BAD_REQUEST, 'Parameter Error')); 189 | 190 | default: 191 | return next(createError(sc.INTERNAL_SERVER_ERROR)); 192 | } 193 | } 194 | }, 195 | 196 | modifyWeathy: async (req, res, next) => { 197 | try { 198 | if (!req.body.weathy) throw Error(exception.BAD_REQUEST); 199 | 200 | const weathyParams = JSON.parse(req.body.weathy); 201 | const { weathyId } = req.params; 202 | const { code, clothes, stampId, feedback, isDelete } = weathyParams; 203 | const isExistFile = req.file ? true : false; 204 | const userId = res.locals.userId; 205 | 206 | const checkOwnerClothes = await weathyService.checkOwnerClothes( 207 | clothes, 208 | userId 209 | ); 210 | 211 | if (!checkOwnerClothes) { 212 | return next( 213 | createError( 214 | sc.NO_AUTHORITY, 215 | '옷에 대한 접근 권한이 없습니다.' 216 | ) 217 | ); 218 | } 219 | 220 | const weathy = await weathyService.modifyWeathy( 221 | weathyId, 222 | userId, 223 | code, 224 | clothes, 225 | stampId, 226 | feedback || null 227 | ); 228 | 229 | if (!weathy) throw Error(exception.NO_AUTHORITY); 230 | 231 | if (isExistFile && !isDelete) { 232 | //수정 시 233 | const imgUrl = await uploadS3(userId, req.file.buffer); 234 | await weathyService.modifyImgField(imgUrl, weathyId, userId); 235 | } else if (!isExistFile && isDelete) { 236 | //삭제 시 237 | await weathyService.modifyImgField(null, weathyId, userId); 238 | } 239 | 240 | res.status(sc.OK).json({ 241 | message: '웨디 기록 수정 완료' 242 | }); 243 | next(); 244 | } catch (err) { 245 | logger.error(err.stack); 246 | switch (err.message) { 247 | case exception.NO_DAILY_WEATHER: 248 | return next( 249 | createError( 250 | sc.BAD_REQUEST, 251 | 'Daily weather 데이터가 존재하지 않습니다.' 252 | ) 253 | ); 254 | case exception.NO_AUTHORITY: 255 | return next( 256 | createError( 257 | sc.BAD_REQUEST, 258 | '웨디를 수정할 수 없습니다.' 259 | ) 260 | ); 261 | case exception.DUPLICATION_WEATHY: 262 | return next( 263 | createError( 264 | sc.BAD_REQUEST, 265 | '잘못된 날짜에 Weathy 작성(중복된 웨디 작성)' 266 | ) 267 | ); 268 | case exception.BAD_REQUEST: 269 | return next(createError(sc.BAD_REQUEST, 'Parameter Error')); 270 | case exception.CANNOT_UPLOAD_FILE: 271 | return next( 272 | createError( 273 | sc.INTERNAL_SERVER_ERROR, 274 | 'Cannot upload File' 275 | ) 276 | ); 277 | default: 278 | return next(createError(sc.INTERNAL_SERVER_ERROR)); 279 | } 280 | } 281 | }, 282 | 283 | deleteWeathy: async (req, res, next) => { 284 | const { weathyId } = req.params; 285 | const userId = res.locals.userId; 286 | 287 | try { 288 | const deletedWeathy = await weathyService.deleteWeathy( 289 | weathyId, 290 | userId 291 | ); 292 | 293 | if (!deletedWeathy) { 294 | res.status(sc.NO_CONTENTS).json({}); 295 | next(); 296 | } else { 297 | res.status(sc.OK).json({ 298 | message: '웨디 기록 삭제 성공' 299 | }); 300 | next(); 301 | } 302 | } catch (err) { 303 | switch (err) { 304 | default: 305 | return next(createError(sc.INTERNAL_SERVER_ERROR)); 306 | } 307 | } 308 | } 309 | }; 310 | -------------------------------------------------------------------------------- /WeathyServer/models/category.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'ClothesCategories', 4 | { 5 | name: { 6 | type: DataTypes.STRING(45), 7 | allowNull: false 8 | } 9 | }, 10 | { 11 | underscored: true, 12 | freezeTableName: true, 13 | paranoid: true 14 | } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /WeathyServer/models/climate.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'Climates', 4 | { 5 | icon_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false 8 | }, 9 | description: { 10 | type: DataTypes.STRING(100), 11 | allowNull: false 12 | } 13 | }, 14 | { 15 | underscored: true, 16 | freezeTableName: true, 17 | paranoid: true 18 | } 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /WeathyServer/models/climateMessage.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'ClimateMessages', 4 | { 5 | weather_group: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false 8 | }, 9 | description: { 10 | type: DataTypes.STRING(100), 11 | allowNull: false 12 | } 13 | }, 14 | { 15 | underscored: true, 16 | freezeTableName: true, 17 | paranoid: true, 18 | indexes: [ 19 | { 20 | unique: true, 21 | fields: ['weather_group', 'description'] 22 | } 23 | ] 24 | } 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /WeathyServer/models/clothes.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'Clothes', 4 | { 5 | user_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false 8 | }, 9 | name: { 10 | type: DataTypes.STRING(45), 11 | allowNull: false 12 | }, 13 | is_deleted: { 14 | type: DataTypes.TINYINT(1), 15 | allowNull: false 16 | } 17 | }, 18 | { 19 | underscored: true, 20 | freezeTableName: true, 21 | paranoid: true 22 | } 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /WeathyServer/models/dailyWeather.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'DailyWeathers', 4 | { 5 | date: { 6 | type: DataTypes.DATE, 7 | allowNull: false 8 | }, 9 | temperature_max: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false 12 | }, 13 | temperature_min: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false 16 | }, 17 | humidity: { 18 | type: DataTypes.INTEGER, 19 | allowNull: false 20 | }, 21 | precipitation: { 22 | type: DataTypes.INTEGER, 23 | allowNull: false 24 | }, 25 | wind_speed: { 26 | type: DataTypes.FLOAT, 27 | allowNull: false 28 | }, 29 | wind_direction: { 30 | type: DataTypes.INTEGER, 31 | allowNull: false 32 | } 33 | }, 34 | { 35 | underscored: true, 36 | freezeTableName: true, 37 | paranoid: true, 38 | indexes: [ 39 | { 40 | unique: true, 41 | fields: ['location_id', 'date'] 42 | } 43 | ] 44 | } 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /WeathyServer/models/hourlyWeather.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'HourlyWeathers', 4 | { 5 | date: { 6 | type: DataTypes.DATE, 7 | allowNull: false 8 | }, 9 | hour: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false 12 | }, 13 | temperature: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false 16 | }, 17 | pop: { 18 | type: DataTypes.INTEGER, 19 | allowNull: false 20 | } 21 | }, 22 | { 23 | underscored: true, 24 | freezeTableName: true, 25 | paranoid: true, 26 | indexes: [ 27 | { 28 | unique: true, 29 | fields: ['date', 'hour', 'location_id'] 30 | } 31 | ] 32 | } 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /WeathyServer/models/index.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const env = process.env.NODE_ENV || 'development'; 3 | const config = require('../config/database.json')[env]; 4 | const db = {}; 5 | 6 | let sequelize; 7 | 8 | if (config.use_env_variable) { 9 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 10 | } else { 11 | sequelize = new Sequelize( 12 | config.database, 13 | config.username, 14 | config.password, 15 | config 16 | ); 17 | } 18 | 19 | db.sequelize = sequelize; 20 | db.Sequelize = Sequelize; 21 | 22 | db.HourlyWeather = require('./hourlyWeather')(sequelize, Sequelize); 23 | db.DailyWeather = require('./dailyWeather')(sequelize, Sequelize); 24 | db.Location = require('./location')(sequelize, Sequelize); 25 | db.Climate = require('./climate')(sequelize, Sequelize); 26 | db.ClimateMessage = require('./climateMessage')(sequelize, Sequelize); 27 | db.User = require('./user')(sequelize, Sequelize); 28 | db.ClothesCategory = require('./category')(sequelize, Sequelize); 29 | db.Clothes = require('./clothes')(sequelize, Sequelize); 30 | db.WeathyClothes = require('./weathyClothes')(sequelize, Sequelize); 31 | db.Weathy = require('./weathy')(sequelize, Sequelize); 32 | db.Token = require('./token')(sequelize, Sequelize); 33 | 34 | db.Location.hasMany( 35 | db.HourlyWeather, 36 | { foreignKey: 'location_id' }, 37 | { onDelete: 'RESTRICT' } 38 | ); 39 | db.HourlyWeather.belongsTo(db.Location, { 40 | foreignKey: 'location_id' 41 | }); 42 | 43 | db.Location.hasMany( 44 | db.HourlyWeather, 45 | { foreignKey: 'location_id' }, 46 | { onDelete: 'RESTRICT' } 47 | ); 48 | db.DailyWeather.belongsTo(db.Location, { 49 | foreignKey: 'location_id' 50 | }); 51 | 52 | db.Climate.hasMany( 53 | db.DailyWeather, 54 | { foreignKey: 'climate_id' }, 55 | { onDelete: 'RESTRICT' } 56 | ); 57 | db.DailyWeather.belongsTo(db.Climate, { 58 | foreignKey: 'climate_id' 59 | }); 60 | 61 | db.Climate.hasMany( 62 | db.HourlyWeather, 63 | { foreignKey: 'climate_id' }, 64 | { onDelete: 'RESTRICT' } 65 | ); 66 | db.HourlyWeather.belongsTo(db.Climate, { foreignKey: 'climate_id' }); 67 | 68 | db.User.hasMany(db.Weathy, { foreignKey: 'user_id' }, { onDelete: 'CASCADE' }); 69 | db.Weathy.belongsTo(db.User, { foreignKey: 'user_id' }); 70 | 71 | db.User.hasMany(db.Clothes, { foreignKey: 'user_id' }, { onDelete: 'CASCADE' }); 72 | db.Clothes.belongsTo(db.User, { foreignKey: 'user_id' }); 73 | 74 | db.ClothesCategory.hasMany(db.Clothes, { 75 | foreignKey: 'category_id' 76 | }); 77 | db.Clothes.belongsTo(db.ClothesCategory, { 78 | foreignKey: 'category_id' 79 | }); 80 | 81 | db.Clothes.hasMany(db.WeathyClothes, { foreignKey: 'clothes_id' }); 82 | db.WeathyClothes.belongsTo(db.Clothes, { foreignKey: 'clothes_id' }); 83 | 84 | db.Weathy.hasMany( 85 | db.WeathyClothes, 86 | { foreignKey: 'weathy_id', onDelete: 'CASCADE' } 87 | // { onDelete: 'CASCADE' } 88 | ); 89 | db.WeathyClothes.belongsTo(db.Weathy, { foreignKey: 'weathy_id' }); 90 | 91 | db.DailyWeather.hasMany( 92 | db.Weathy, 93 | { foreignKey: 'dailyweather_id' }, 94 | { onDelete: 'cascade' } 95 | ); 96 | db.Weathy.belongsTo(db.DailyWeather, { foreignKey: 'dailyweather_id' }); 97 | 98 | db.User.hasOne(db.Token, { foreignKey: 'user_id' }, { onDelete: 'cascade' }); 99 | db.Token.belongsTo(db.User, { foreignKey: 'user_id' }); 100 | 101 | module.exports = db; 102 | -------------------------------------------------------------------------------- /WeathyServer/models/location.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | return sequelize.define( 5 | 'Locations', 6 | { 7 | id: { 8 | type: Sequelize.BIGINT, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | name: { 13 | type: DataTypes.STRING(45), 14 | allowNull: false 15 | }, 16 | lat: { 17 | type: DataTypes.DOUBLE, 18 | allowNull: false 19 | }, 20 | lng: { 21 | type: DataTypes.DOUBLE, 22 | allowNull: false 23 | } 24 | }, 25 | { 26 | underscored: true, 27 | freezeTableName: true, 28 | paranoid: true 29 | } 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /WeathyServer/models/token.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'Token', 4 | { 5 | token: { 6 | type: DataTypes.STRING(45), 7 | allowNull: false 8 | } 9 | }, 10 | { 11 | underscored: true, 12 | freezeTableName: true 13 | } 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /WeathyServer/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'Users', 4 | { 5 | nickname: { 6 | type: DataTypes.STRING(8), 7 | allowNull: false 8 | }, 9 | uuid: { 10 | type: DataTypes.STRING(45), 11 | allowNull: false 12 | } 13 | }, 14 | { 15 | underscored: true, 16 | freezeTableName: true, 17 | paranoid: true 18 | } 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /WeathyServer/models/weathy.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'Weathies', 4 | { 5 | emoji_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false 8 | }, 9 | description: { 10 | type: DataTypes.STRING(500), 11 | allowNull: true 12 | }, 13 | img_url: { 14 | type: DataTypes.STRING(500), 15 | allowNull: true 16 | } 17 | }, 18 | { 19 | underscored: true, 20 | freezeTableName: true 21 | } 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /WeathyServer/models/weathyClothes.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | 'WeathyClothes', 4 | {}, 5 | { 6 | underscored: true, 7 | freezeTableName: true 8 | } 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /WeathyServer/modules/exception.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // COMMON 3 | NO_DATA: 'NO_DATA', 4 | 5 | // TOKEN 6 | EXPIRED_TOKEN: 'EXPIRED_TOKEN', 7 | INVALID_TOKEN: 'INVALID_TOKEN', 8 | MISMATCH_TOKEN: 'MISMATCH_TOKEN', 9 | 10 | NO_AUTHORITY: 'NO_AUTHORITY', 11 | // USER 12 | NO_USER: 'NO_USER', 13 | ALREADY_USER: 'ALREADY_USER', 14 | 15 | // LOCATION 16 | INVALID_LOCATION: 'INVALID_LOCATION', 17 | 18 | // WEAHTER: 19 | NO_DAILY_WEATHER: 'NO_DAILY_WEATHER_DATA', 20 | 21 | // WEATHY 22 | DUPLICATION_WEATHY: 'DUPLICATION_WEATHRY_ERROR', 23 | 24 | // CLOTHES 25 | ALREADY_CLOTHES: 'ALREADY_CLOTHES', 26 | NO_CLOTHES: 'NO_CLOTHES', 27 | NOT_AUTHORIZED_CLOTHES: 'NOT_AUTHORIZED_CLOTHES', 28 | 29 | // REQUEST 30 | BAD_REQUEST: 'BAD_REQUEST', 31 | 32 | CANNOT_UPLOAD_FILE: 'CANNOT_UPLOAD_FILE', 33 | // SERVER 34 | SERVER_ERROR: 'SERVER_ ERROR' 35 | }; 36 | -------------------------------------------------------------------------------- /WeathyServer/modules/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | const winstonDaily = require('winston-daily-rotate-file'); 3 | 4 | const appRoot = require('app-root-path'); 5 | const fs = require('fs'); 6 | 7 | const env = process.env.NODE_ENV || 'development'; 8 | const logDir = `${appRoot}/logs`; 9 | 10 | // Create the log directory if it does not exist 11 | if (!fs.existsSync(logDir)) { 12 | fs.mkdirSync(logDir); 13 | } 14 | const { combine, timestamp, label, printf } = format; 15 | const logFormat = printf(({ level, message, label, timestamp }) => { 16 | return `${timestamp} [${label}] ${level}: ${message}`; 17 | }); 18 | 19 | const logger = createLogger({ 20 | level: 'info', 21 | format: combine( 22 | label({ 23 | label: 'Weathy' 24 | }), 25 | timestamp({ 26 | format: 'YYYY-MM-DD HH:mm:ss' 27 | }), 28 | logFormat 29 | ), 30 | transports: [ 31 | new winstonDaily({ 32 | level: 'info', 33 | datePattern: 'YYYY-MM-DD', 34 | dirname: logDir, 35 | filename: `%DATE%.log`, 36 | maxFiles: 30, 37 | zippedArchive: true 38 | }), 39 | new winstonDaily({ 40 | level: 'error', 41 | datePattern: 'YYYY-MM-DD', 42 | dirname: logDir, 43 | filename: `%DATE%.error.log`, 44 | maxFiles: 30, 45 | zippedArchive: true 46 | }) 47 | ] 48 | }); 49 | 50 | if (env !== 'production') { 51 | logger.add( 52 | new transports.Console({ 53 | format: format.combine(format.colorize(), format.simple()) 54 | }) 55 | ); 56 | } 57 | module.exports = logger; 58 | -------------------------------------------------------------------------------- /WeathyServer/modules/statusCode.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OK: 200, 3 | 4 | NO_CONTENTS: 204, 5 | INVALID_ACCOUNT: 401, 6 | 7 | // Error status code 8 | BAD_REQUEST: 400, 9 | NO_AUTHORITY: 403, 10 | INTERNAL_SERVER_ERROR: 500 11 | }; 12 | -------------------------------------------------------------------------------- /WeathyServer/modules/swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerUi = require('swagger-ui-express'); 2 | const swaggereJsdoc = require('swagger-jsdoc'); 3 | 4 | const options = { 5 | swaggerDefinition: { 6 | info: { 7 | title: 'Weathy API', 8 | version: '1.0.0', 9 | description: 'Weathy API with express' 10 | }, 11 | host: 'localhost:3000', 12 | basePath: '/' 13 | }, 14 | apis: ['./routes/*.js', './swagger/*'] 15 | }; 16 | 17 | const specs = swaggereJsdoc(options); 18 | 19 | module.exports = { 20 | swaggerUi, 21 | specs 22 | }; 23 | -------------------------------------------------------------------------------- /WeathyServer/modules/tokenMiddleware.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const dayjs = require('dayjs'); 3 | const { Token, sequelize } = require('../models'); 4 | const sc = require('./statusCode'); 5 | const exception = require('./exception'); 6 | const { generateToken, isUserOwnerOfToken } = require('../utils/tokenUtils'); 7 | 8 | const TOKEN_EXPIRES_IN_DAYS = 15; // 토큰 유효 기간 (15일) 9 | 10 | const validateToken = async (req, res, next) => { 11 | // 토큰 검사 12 | // 1. header에 token이 있는지 확인 13 | const token = req.headers['x-access-token']; 14 | if (!token) { 15 | return next(createError(sc.BAD_REQUEST, 'No Token')); 16 | } 17 | 18 | // 2. 이 토큰이 유효한지 확인 19 | const userToken = await Token.findOne({ where: { token: token } }); 20 | // 2-1. token 객체가 DB에 존재해야 한다 21 | if (userToken === null) { 22 | return next(createError(sc.INVALID_ACCOUNT, 'No Matching Token on DB')); 23 | } else if (!isUserOwnerOfToken(userToken.user_id, token)) { 24 | // 2-2. 객체의 userId와 토큰에 포함된 id가 같아야 한다 25 | return next(createError(sc.INVALID_ACCOUNT, 'Token user_id is wrong')); 26 | } else { 27 | // 2-3. param에 userId가 있다면, 토큰에 포함된 id와 일치하는지 확인. 없으면 그냥 넘어간다 28 | // 가정: param에 userId가 없다면, 그 API는 param에 userId를 요구하지 않는 것이다 29 | // param에 userId가 있어야 하는데 없는 경우는, service 단에서 error를 처리해야 함 30 | const { userId } = req.params; 31 | if (userId && !isUserOwnerOfToken(userId, token)) { 32 | return next( 33 | createError( 34 | sc.INVALID_ACCOUNT, 35 | 'Param userId is different with Token' 36 | ) 37 | ); 38 | } 39 | 40 | // 2-4. token의 만료 기한이 지나지 않았어야 한다 41 | const updatedTime = userToken.updatedAt; 42 | const updatedTimeDayjs = dayjs(updatedTime); 43 | const expirationTime = updatedTimeDayjs.add( 44 | TOKEN_EXPIRES_IN_DAYS, 45 | 'days' 46 | ); 47 | const now = dayjs(new Date()); 48 | if (!now.isBefore(expirationTime)) { 49 | return next(createError(sc.INVALID_ACCOUNT, 'Expired Token')); 50 | } 51 | } 52 | 53 | // 3. res.locals에 token 값과 userId 저장해 둠 54 | res.locals.tokenValue = token; 55 | res.locals.userId = userToken.user_id; 56 | next(); 57 | }; 58 | 59 | const updateToken = async (req, res) => { 60 | // 토큰 업데이트 61 | // 사용되면 return res.send() 에서 return을 뺄 것 62 | 63 | try { 64 | const token = res.locals.tokenValue; 65 | 66 | const userToken = await Token.findOne({ where: { token: token } }); 67 | 68 | userToken.changed('updatedAt', true); 69 | await userToken.update({ updatedAt: new Date() }); 70 | 71 | return res; 72 | } catch (err) { 73 | console.log(err); 74 | throw Error(exception.SERVER_ERROR); 75 | } 76 | }; 77 | 78 | module.exports = { 79 | validateToken, 80 | updateToken 81 | }; 82 | -------------------------------------------------------------------------------- /WeathyServer/modules/uploadFile.js: -------------------------------------------------------------------------------- 1 | const aws = require('aws-sdk'); 2 | 3 | const logger = require('winston'); 4 | 5 | const exception = require('./exception'); 6 | 7 | aws.config.loadFromPath(__dirname + '/../config/s3.json'); 8 | 9 | const s3 = new aws.S3(); 10 | 11 | const uploadS3 = (userId, file) => { 12 | const param = { 13 | Bucket: 'weathy', 14 | Key: `ootd/${userId}/${Date.now()}.jpeg`, 15 | ACL: 'public-read', 16 | Body: file, 17 | ContentType: 'image/jpeg' 18 | }; 19 | 20 | return new Promise((res, rej) => { 21 | s3.upload(param, function (err, data) { 22 | if (err) { 23 | logger.error(err); 24 | rej(Error(exception.SERVER_ERROR)); 25 | } else { 26 | res(data.Location); 27 | } 28 | }); 29 | }); 30 | }; 31 | 32 | module.exports = { 33 | uploadS3 34 | }; 35 | -------------------------------------------------------------------------------- /WeathyServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weathyserver", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "test": "node_modules/.bin/mocha $(find tests/ -name '*.js') --recursive -w" 8 | }, 9 | "dependencies": { 10 | "app-root-path": "^3.0.0", 11 | "aws-sdk": "^2.848.0", 12 | "cookie-parser": "~1.4.4", 13 | "crypto-random-string": "^3.3.0", 14 | "dayjs": "^1.10.2", 15 | "debug": "~2.6.9", 16 | "express": "~4.16.1", 17 | "http-errors": "~1.6.3", 18 | "jade": "~1.11.0", 19 | "morgan": "~1.9.1", 20 | "multer": "^1.4.2", 21 | "multer-s3": "^2.9.0", 22 | "mysql2": "^2.2.5", 23 | "node-mocks-http": "^1.10.1", 24 | "request": "^2.88.2", 25 | "request-promise": "^4.2.6", 26 | "sequelize": "^6.3.5", 27 | "sequelize-cli": "^6.2.0", 28 | "winston": "^3.3.3", 29 | "winston-daily-rotate-file": "^4.5.0" 30 | }, 31 | "devDependencies": { 32 | "decache": "^4.6.0", 33 | "eslint": "^7.16.0", 34 | "eslint-config-prettier": "^7.1.0", 35 | "eslint-plugin-prettier": "^3.3.0", 36 | "mocha": "^8.2.1", 37 | "prettier": "2.2.1", 38 | "swagger-jsdoc": "^6.0.0", 39 | "swagger-ui-express": "^4.1.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WeathyServer/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /WeathyServer/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const authController = require('../controllers/authController'); 4 | const { updateToken } = require('../modules/tokenMiddleware'); 5 | 6 | router.post('/login', authController.login, updateToken); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /WeathyServer/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function (req, res) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /WeathyServer/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const weathyController = require('../controllers/weathyController'); 5 | const { validateToken, updateToken } = require('../modules/tokenMiddleware'); 6 | const userController = require('../controllers/userController'); 7 | const clothesController = require('../controllers/clothesController'); 8 | const calendarController = require('../controllers/calendarController'); 9 | 10 | router.post('/', userController.createUser); 11 | router.put('/:userId', validateToken, userController.modifyUser, updateToken); 12 | router.get( 13 | '/:userId/clothes', 14 | validateToken, 15 | clothesController.getClothes, 16 | updateToken 17 | ); 18 | router.post( 19 | '/:userId/clothes', 20 | validateToken, 21 | clothesController.addClothes, 22 | updateToken 23 | ); 24 | router.delete( 25 | '/:userId/clothes', 26 | validateToken, 27 | clothesController.deleteClothes, 28 | updateToken 29 | ); 30 | 31 | router.get( 32 | '/:userId/weathy/recommend', 33 | validateToken, 34 | weathyController.getRecommendedWeathy, 35 | updateToken 36 | ); 37 | router.get( 38 | '/:userId/calendar', 39 | validateToken, 40 | calendarController.getCalendarOverviews, 41 | updateToken 42 | ); 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /WeathyServer/routes/weather.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const weatherController = require('../controllers/weatherController'); 4 | 5 | router.get('/overview', weatherController.getWeatherByLocation); 6 | router.get('/forecast/hourly', weatherController.getHourlyWeatherForecast); 7 | router.get('/forecast/daily', weatherController.getDailyWeatherForecast); 8 | router.get('/daily/extra', weatherController.getExtraDailyWeather); 9 | router.get('/overviews', weatherController.getWeathersByKeyword); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /WeathyServer/routes/weathy.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const weathyController = require('../controllers/weathyController'); 5 | const { validateToken, updateToken } = require('../modules/tokenMiddleware'); 6 | const multer = require('multer'); 7 | 8 | router.get('/', validateToken, weathyController.getWeathy, updateToken); 9 | router.post( 10 | '/', 11 | validateToken, 12 | multer().single('img'), 13 | weathyController.createWeathy, 14 | updateToken 15 | ); 16 | router.put( 17 | '/:weathyId', 18 | validateToken, 19 | multer().single('img'), 20 | weathyController.modifyWeathy, 21 | updateToken 22 | ); 23 | router.delete( 24 | '/:weathyId', 25 | validateToken, 26 | weathyController.deleteWeathy, 27 | updateToken 28 | ); 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /WeathyServer/services/calendarService.js: -------------------------------------------------------------------------------- 1 | const { DailyWeather, Weathy } = require('../models'); 2 | const Sequelize = require('sequelize'); 3 | const { literal } = require('sequelize'); 4 | 5 | module.exports = { 6 | getValidCalendarOverviewList: async (userId, startDate, endDate) => { 7 | const Op = Sequelize.Op; 8 | const weathies = await Weathy.findAll({ 9 | include: [ 10 | { 11 | model: DailyWeather, 12 | attributes: [ 13 | 'date', 14 | 'temperature_max', 15 | 'temperature_min', 16 | 'climate_id' 17 | ], 18 | where: { 19 | date: { 20 | [Op.and]: { 21 | [Op.gte]: startDate, 22 | [Op.lte]: endDate 23 | } 24 | } 25 | } 26 | } 27 | ], 28 | where: { 29 | user_id: userId 30 | }, 31 | order: literal('DailyWeather.date ASC') 32 | }); 33 | let validCalendarOverviewList = []; 34 | for (let i = 0; i < weathies.length; ++i) { 35 | const weathy = weathies[i]; 36 | validCalendarOverviewList.push({ 37 | id: weathy.id, 38 | date: weathy.DailyWeather.date, 39 | climateIconId: weathy.DailyWeather.climate_id, 40 | stampId: weathy.emoji_id, 41 | temperature: { 42 | maxTemp: weathy.DailyWeather.temperature_max, 43 | minTemp: weathy.DailyWeather.temperature_min 44 | } 45 | }); 46 | } 47 | return validCalendarOverviewList; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /WeathyServer/services/climateService.js: -------------------------------------------------------------------------------- 1 | const { Climate, ClimateMessage } = require('../models'); 2 | const exception = require('../modules/exception'); 3 | 4 | const getIconId = async (climate_id) => { 5 | const climate = await Climate.findOne({ where: { id: climate_id } }); 6 | if (!climate) { 7 | throw Error(exception.NO_DATA); 8 | } 9 | return climate.icon_id; 10 | }; 11 | 12 | const getTemperatureLevel = (temperature) => { 13 | let idx = parseInt(temperature / 10) + 3; 14 | idx = Math.max(1, idx); 15 | idx = Math.min(6, idx); 16 | return idx; 17 | }; 18 | 19 | const getWeatherGroup = (climate_id, temperature) => { 20 | climate_id %= 100; 21 | if (climate_id == 1) { 22 | return getTemperatureLevel(temperature); 23 | } else if (climate_id == 2 || climate_id == 3) { 24 | return 6 + getTemperatureLevel(temperature); 25 | } else if (climate_id == 4) { 26 | return 12 + getTemperatureLevel(temperature); 27 | } else if (climate_id == 9 || climate_id == 10) { 28 | return 19; 29 | } else if (climate_id == 11) { 30 | return 20; 31 | } else if (climate_id == 13) { 32 | return 21; 33 | } else if (climate_id == 50) { 34 | return 22; 35 | } 36 | throw Error(exception.NO_DATA); 37 | }; 38 | 39 | const getDescription = async (climate_id, temperature) => { 40 | const weather_group = await getWeatherGroup(climate_id, temperature); 41 | const climateMessages = await ClimateMessage.findAll({ 42 | where: { weather_group: weather_group } 43 | }); 44 | if (climateMessages.length == 0) { 45 | throw Error(exception.NO_DATA); 46 | } 47 | 48 | const rand = Math.random(); 49 | const idx = Math.floor(rand * climateMessages.length); 50 | return climateMessages[idx].description; 51 | }; 52 | 53 | const getClimateByIconId = async (id) => { 54 | const climate = await Climate.findOne({ 55 | where: { 56 | icon_id: id 57 | } 58 | }); 59 | const iconId = climate.icon_id; 60 | const description = climate.description; 61 | 62 | return { 63 | iconId, 64 | description 65 | }; 66 | }; 67 | module.exports = { 68 | getClimate: async (id, temperature) => { 69 | const icon_id = await getIconId(id); 70 | const description = await getDescription(id, temperature); 71 | return { 72 | iconId: icon_id, 73 | description: description 74 | }; 75 | }, 76 | getIconId, 77 | getClimateByIconId 78 | }; 79 | -------------------------------------------------------------------------------- /WeathyServer/services/clothesService.js: -------------------------------------------------------------------------------- 1 | const { Clothes, ClothesCategory, WeathyClothes } = require('../models'); 2 | const exception = require('../modules/exception'); 3 | const { Op } = require('sequelize'); 4 | 5 | const setClothesForm = async () => { 6 | const closet = {}; 7 | 8 | const clothesCategories = await ClothesCategory.findAll({ 9 | attributes: ['name', 'id'] 10 | }); 11 | 12 | for (const c of clothesCategories) { 13 | closet[c.name] = { 14 | categoryId: c.id, 15 | clothes: [] 16 | }; 17 | } 18 | return closet; 19 | }; 20 | 21 | const unionTwoCloset = (closet1, closet2) => { 22 | const top1 = closet1.top.clothes; 23 | const top2 = closet2.top.clothes; 24 | const unionTop = top2.concat(top1).filter(function (cl) { 25 | return this.has(cl.id) ? false : this.add(cl.id); 26 | }, new Set()); 27 | const bottom1 = closet1.bottom.clothes; 28 | const bottom2 = closet2.bottom.clothes; 29 | const unionBottom = bottom2.concat(bottom1).filter(function (cl) { 30 | return this.has(cl.id) ? false : this.add(cl.id); 31 | }, new Set()); 32 | const outer1 = closet1.outer.clothes; 33 | const outer2 = closet2.outer.clothes; 34 | const unionOuter = outer2.concat(outer1).filter(function (cl) { 35 | return this.has(cl.id) ? false : this.add(cl.id); 36 | }, new Set()); 37 | const etc1 = closet1.etc.clothes; 38 | const etc2 = closet2.etc.clothes; 39 | const unionEtc = etc2.concat(etc1).filter(function (cl) { 40 | return this.has(cl.id) ? false : this.add(cl.id); 41 | }, new Set()); 42 | 43 | const unionCloset = { 44 | top: { 45 | categoryId: 1, 46 | clothesNum: top1.length, 47 | clothes: unionTop 48 | }, 49 | bottom: { 50 | categoryId: 2, 51 | clothesNum: bottom1.length, 52 | clothes: unionBottom 53 | }, 54 | outer: { 55 | categoryId: 3, 56 | clothesNum: outer1.length, 57 | clothes: unionOuter 58 | }, 59 | etc: { 60 | categoryId: 4, 61 | clothesNum: etc1.length, 62 | clothes: unionEtc 63 | } 64 | }; 65 | return unionCloset; 66 | }; 67 | 68 | async function getClothesNumByUserId(userId) { 69 | const aliveClothes = await Clothes.findAndCountAll({ 70 | where: { 71 | user_id: userId, 72 | is_deleted: 0 73 | } 74 | }); 75 | return aliveClothes.count; 76 | } 77 | 78 | async function getClothesByUserId(userId) { 79 | const responseCloset = new Object(); 80 | const clothesCategories = await ClothesCategory.findAll(); 81 | 82 | for (const category of clothesCategories) { 83 | const categoryClothes = await Clothes.findAll({ 84 | where: { 85 | user_id: userId, 86 | category_id: category.id, 87 | is_deleted: 0 88 | }, 89 | order: [ 90 | ['updated_at', 'DESC'], 91 | ['id', 'DESC'] 92 | ] 93 | }); 94 | 95 | const categoryCloset = new Object(); 96 | categoryCloset.categoryId = category.id; 97 | const categoryClothesList = new Array(); 98 | await categoryClothes.forEach((element) => { 99 | const clothes = new Object(); 100 | clothes.id = element.id; 101 | clothes.name = element.name; 102 | categoryClothesList.push(clothes); 103 | }); 104 | categoryCloset.clothes = categoryClothesList; 105 | categoryCloset.clothesNum = categoryClothesList.length; 106 | responseCloset[category.name] = categoryCloset; 107 | } 108 | return responseCloset; 109 | } 110 | 111 | async function getClothesByWeathyId(userId, weathyId) { 112 | const responseCloset = new Object(); 113 | const clothesCategories = await ClothesCategory.findAll(); 114 | 115 | const weathyClothes = await WeathyClothes.findAll({ 116 | where: { 117 | weathy_id: weathyId 118 | }, 119 | attributes: ['clothes_id'] 120 | }); 121 | const weathyClothesIdList = new Array(); 122 | await weathyClothes.forEach((e) => { 123 | weathyClothesIdList.push(e.dataValues.clothes_id); 124 | }); 125 | 126 | for (const category of clothesCategories) { 127 | const categoryClothes = await Clothes.findAll({ 128 | where: { 129 | user_id: userId, 130 | category_id: category.id, 131 | id: { 132 | [Op.in]: weathyClothesIdList 133 | } 134 | }, 135 | order: [ 136 | ['updated_at', 'DESC'], 137 | ['id', 'DESC'] 138 | ] 139 | }); 140 | 141 | const categoryCloset = new Object(); 142 | categoryCloset.categoryId = category.id; 143 | const categoryClothesList = new Array(); 144 | await categoryClothes.forEach((element) => { 145 | const clothes = new Object(); 146 | clothes.id = element.id; 147 | clothes.name = element.name; 148 | categoryClothesList.push(clothes); 149 | }); 150 | categoryCloset.clothes = categoryClothesList; 151 | categoryCloset.clothesNum = categoryClothesList.length; 152 | responseCloset[category.name] = categoryCloset; 153 | } 154 | return responseCloset; 155 | } 156 | 157 | async function getWeathyCloset(weathyId) { 158 | try { 159 | const closet = await setClothesForm(); 160 | 161 | const weathyClothes = await WeathyClothes.findAll({ 162 | include: [ 163 | { 164 | model: Clothes, 165 | required: true, 166 | paranoid: false, 167 | attributes: ['id', 'name'], 168 | include: [ 169 | { 170 | model: ClothesCategory, 171 | required: true, 172 | paranoid: false, 173 | attributes: ['id', 'name'] 174 | } 175 | ] 176 | } 177 | ], 178 | where: { 179 | weathy_id: weathyId 180 | } 181 | }); 182 | 183 | for (let wc of weathyClothes) { 184 | const categoryName = wc.Clothe.ClothesCategory.name; 185 | const clothesId = wc.Clothe.id; 186 | const clothesName = wc.Clothe.name; 187 | 188 | closet[categoryName].clothes.push({ 189 | id: clothesId, 190 | name: clothesName 191 | }); 192 | } 193 | 194 | for (let category of Object.keys(closet)) { 195 | closet[category].clothesNum = closet[category].clothes.length; 196 | } 197 | 198 | return closet; 199 | } catch (err) { 200 | throw Error(exception.SERVER_ERROR); 201 | } 202 | } 203 | 204 | async function addClothesByUserId(userId, category, name) { 205 | // 이미 한 번 지워졌었던 값인지 확인하는 작업이 필요하다 206 | const alreadyClothes = await Clothes.findOne({ 207 | where: { user_id: userId, category_id: category, name: name } 208 | }); 209 | 210 | if (alreadyClothes === null) { 211 | await Clothes.create({ 212 | user_id: userId, 213 | category_id: category, 214 | name: name, 215 | is_deleted: 0 216 | }); 217 | } else if (alreadyClothes.is_deleted === 0) { 218 | throw Error(exception.ALREADY_CLOTHES); 219 | } else { 220 | await Clothes.update( 221 | { is_deleted: 0 }, 222 | { 223 | where: { 224 | user_id: userId, 225 | category_id: category, 226 | name: name 227 | } 228 | } 229 | ); 230 | } 231 | 232 | const responseCloset = new Object(); 233 | const clothesCategories = await ClothesCategory.findAll(); 234 | 235 | for (const ca of clothesCategories) { 236 | const categoryClothes = await Clothes.findAll({ 237 | where: { 238 | user_id: userId, 239 | category_id: ca.id, 240 | is_deleted: 0 241 | }, 242 | order: [ 243 | ['updated_at', 'DESC'], 244 | ['id', 'DESC'] 245 | ] 246 | }); 247 | const categoryCloset = new Object(); 248 | categoryCloset.categoryId = ca.id; 249 | const categoryClothesList = new Array(); 250 | await categoryClothes.forEach((element) => { 251 | const clothes = new Object(); 252 | clothes.id = element.id; 253 | clothes.name = element.name; 254 | categoryClothesList.push(clothes); 255 | }); 256 | if (ca.id === category) { 257 | categoryCloset.clothes = categoryClothesList; 258 | } else { 259 | categoryCloset.clothes = []; 260 | } 261 | categoryCloset.clothesNum = categoryCloset.clothes.length; 262 | responseCloset[ca.name] = categoryCloset; 263 | } 264 | return responseCloset; 265 | } 266 | 267 | async function deleteClothesByUserId(userId, clothesList) { 268 | // If there are invalid clothes in clothesList, throw error 269 | for (let c in clothesList) { 270 | let cl = await Clothes.findOne({ 271 | where: { user_id: userId, id: clothesList[c] } 272 | }); 273 | 274 | if (cl === null || cl.is_deleted === 1) { 275 | throw Error(exception.NO_CLOTHES); 276 | } 277 | } 278 | 279 | await Clothes.update( 280 | { 281 | is_deleted: 1 282 | }, 283 | { 284 | where: { 285 | id: { 286 | [Op.in]: clothesList 287 | } 288 | } 289 | } 290 | ); 291 | 292 | const responseCloset = await getClothesByUserId(userId); 293 | return responseCloset; 294 | } 295 | 296 | async function createClothesByName(userId, category, name) { 297 | // name으로 userId의 clothes 만들기 298 | await Clothes.create({ 299 | user_id: userId, 300 | category_id: category, 301 | name: name, 302 | is_deleted: 0 303 | }); 304 | } 305 | 306 | async function createDefaultClothes(userId) { 307 | // 유저 생성될 때 기본으로 생성되는 clothes들 만들기 308 | await createClothesByName(userId, 1, '니트'); 309 | await createClothesByName(userId, 1, '후드티'); 310 | await createClothesByName(userId, 1, '티셔츠'); 311 | await createClothesByName(userId, 1, '셔츠'); 312 | await createClothesByName(userId, 1, '블라우스'); 313 | await createClothesByName(userId, 1, '나시'); 314 | await createClothesByName(userId, 2, '청바지'); 315 | await createClothesByName(userId, 2, '슬랙스'); 316 | await createClothesByName(userId, 2, '면바지'); 317 | await createClothesByName(userId, 2, '트레이닝복'); 318 | await createClothesByName(userId, 2, '스커트'); 319 | await createClothesByName(userId, 2, '레깅스'); 320 | await createClothesByName(userId, 3, '패딩'); 321 | await createClothesByName(userId, 3, '코트'); 322 | await createClothesByName(userId, 3, '재킷'); 323 | await createClothesByName(userId, 3, '점퍼'); 324 | await createClothesByName(userId, 3, '가디건'); 325 | await createClothesByName(userId, 3, '경량패딩'); 326 | await createClothesByName(userId, 4, '목도리'); 327 | await createClothesByName(userId, 4, '장갑'); 328 | await createClothesByName(userId, 4, '모자'); 329 | await createClothesByName(userId, 4, '부츠'); 330 | await createClothesByName(userId, 4, '스니커즈'); 331 | await createClothesByName(userId, 4, '로퍼'); 332 | } 333 | 334 | module.exports = { 335 | unionTwoCloset, 336 | getClothesNumByUserId, 337 | getClothesByUserId, 338 | getClothesByWeathyId, 339 | addClothesByUserId, 340 | deleteClothesByUserId, 341 | getWeathyCloset, 342 | createDefaultClothes 343 | }; 344 | -------------------------------------------------------------------------------- /WeathyServer/services/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tokenService: require('./tokenService'), 3 | userService: require('./userService'), 4 | clothesService: require('./clothesService'), 5 | climateService: require('./climateService'), 6 | locationService: require('./locationService'), 7 | weatherService: require('./weatherService'), 8 | weathyService: require('./weathyService'), 9 | calendarService: require('./calendarService') 10 | }; 11 | -------------------------------------------------------------------------------- /WeathyServer/services/locationService.js: -------------------------------------------------------------------------------- 1 | const { Location } = require('../models'); 2 | const Sequelize = require('sequelize'); 3 | const exception = require('../modules/exception'); 4 | const request = require('request-promise'); 5 | const apiKey = require('../config/kakao.json')['apiKey']; 6 | 7 | module.exports = { 8 | getCode: async (lat, lng) => { 9 | let response; 10 | try { 11 | response = await request.get( 12 | 'https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=' + 13 | lng + 14 | '&y=' + 15 | lat, 16 | { 17 | headers: { 18 | Authorization: apiKey 19 | }, 20 | json: true 21 | } 22 | ); 23 | } catch (err) { 24 | throw Error(exception.SERVER_ERROR); 25 | } 26 | 27 | for (let i = 0; i < response.documents.length; ++i) { 28 | if (response.documents[i].region_type == 'H') { 29 | return parseInt(response.documents[i].code / 100000) * 100000; 30 | } 31 | } 32 | }, 33 | getLocationByCode: async (code) => { 34 | const location = await Location.findOne({ where: { id: code } }); 35 | if (!location) { 36 | throw Error(exception.NO_DATA); 37 | } 38 | return { 39 | code: location.id, 40 | name: location.name 41 | }; 42 | }, 43 | getLocationsByKeyword: async (keyword) => { 44 | const Op = Sequelize.Op; 45 | const locations = await Location.findAll({ 46 | where: { 47 | name: { 48 | [Op.like]: '%' + keyword + '%' 49 | } 50 | }, 51 | attributes: [['id', 'code'], 'name'] 52 | }); 53 | return locations; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /WeathyServer/services/tokenService.js: -------------------------------------------------------------------------------- 1 | const { Token } = require('../models'); 2 | const { generateToken } = require('../utils/tokenUtils'); 3 | 4 | module.exports = { 5 | createTokenOfUser: async (user_id) => { 6 | const token = generateToken(user_id); 7 | await Token.create({ 8 | user_id: user_id, 9 | token: token 10 | }); 11 | return token; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /WeathyServer/services/userService.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../models'); 2 | const exception = require('../modules/exception'); 3 | const { createDefaultClothes } = require('../services/clothesService'); 4 | 5 | module.exports = { 6 | getUserByAccount: async (uuid) => { 7 | // uuid로 user 가져오기 8 | const user = await User.findOne({ where: { uuid: uuid } }); 9 | if (user === null) { 10 | throw Error(exception.NO_USER); 11 | } else { 12 | return user; 13 | } 14 | }, 15 | createUserByUuid: async (uuid, nickname) => { 16 | // uuid, nickname 으로 유저 생성 17 | // 이미 같은 uuid를 가진 유저가 있는지 확인 18 | const alreadyUser = await User.findOne({ 19 | where: { 20 | uuid 21 | } 22 | }); 23 | if (alreadyUser != null) { 24 | // 이미 같은 uuid를 가진 유저가 있음 25 | throw Error(exception.ALREADY_USER); 26 | } 27 | 28 | const user = await User.create({ 29 | nickname, 30 | uuid 31 | }); 32 | 33 | await createDefaultClothes(user.id); 34 | return user; 35 | }, 36 | modifyUserById: async (userId, nickname) => { 37 | await User.update({ nickname: nickname }, { where: { id: userId } }); 38 | const user = await User.findOne({ where: { id: userId } }); 39 | return user; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /WeathyServer/services/weatherService.js: -------------------------------------------------------------------------------- 1 | const dateUtils = require('../utils/dateUtils'); 2 | const exception = require('../modules/exception'); 3 | const { DailyWeather, HourlyWeather } = require('../models'); 4 | const climateService = require('./climateService'); 5 | const locationService = require('./locationService'); 6 | 7 | const getRainRating = (value) => { 8 | if (value < 1) return 1; 9 | else if (value < 5) return 2; 10 | else if (value < 20) return 3; 11 | else if (value < 80) return 4; 12 | else if (value < 150) return 5; 13 | else return 6; 14 | }; 15 | 16 | const getHumidityRating = (value) => { 17 | if (value <= 20) return 1; 18 | else if (value <= 40) return 2; 19 | else if (value <= 60) return 3; 20 | else if (value <= 80) return 4; 21 | else if (value <= 100) return 5; 22 | }; 23 | 24 | const getWindRating = (value) => { 25 | if (value <= 1) return 1; 26 | else if (value <= 4) return 2; 27 | else if (value <= 8) return 3; 28 | else if (value <= 12) return 4; 29 | else if (value <= 17) return 5; 30 | else return 6; 31 | }; 32 | 33 | const getDailyWeather = async (code, date) => { 34 | const dailyWeather = await DailyWeather.findOne({ 35 | where: { location_id: code, date: date } 36 | }); 37 | 38 | if (!dailyWeather) { 39 | return null; 40 | } 41 | return { 42 | date: { 43 | year: dateUtils.getYear(dailyWeather.date), 44 | month: dateUtils.getMonth(dailyWeather.date), 45 | day: dateUtils.getDay(dailyWeather.date), 46 | dayOfWeek: dateUtils.getYoil(dailyWeather.date) 47 | }, 48 | temperature: { 49 | maxTemp: dailyWeather.temperature_max, 50 | minTemp: dailyWeather.temperature_min 51 | } 52 | }; 53 | }; 54 | 55 | const getHourlyWeather = async (code, date, hour, timeFormat) => { 56 | const hourlyWeather = await HourlyWeather.findOne({ 57 | where: { location_id: code, date: date, hour: hour } 58 | }); 59 | if (!hourlyWeather) { 60 | return null; 61 | } 62 | return { 63 | time: timeFormat(hourlyWeather.hour), 64 | temperature: hourlyWeather.temperature, 65 | climate: await climateService.getClimate( 66 | hourlyWeather.climate_id, 67 | hourlyWeather.temperature 68 | ), 69 | pop: hourlyWeather.pop 70 | }; 71 | }; 72 | const getDailyWeatherWithClimateIconId = async (code, date) => { 73 | const dailyWeather = await DailyWeather.findOne({ 74 | where: { location_id: code, date: date } 75 | }); 76 | if (!dailyWeather) { 77 | return null; 78 | } 79 | return { 80 | date: { 81 | month: dateUtils.getMonth(dailyWeather.date), 82 | day: dateUtils.getDay(dailyWeather.date), 83 | dayOfWeek: dateUtils.getYoil(dailyWeather.date) 84 | }, 85 | temperature: { 86 | maxTemp: dailyWeather.temperature_max, 87 | minTemp: dailyWeather.temperature_min 88 | }, 89 | climateIconId: dailyWeather.climate_id 90 | }; 91 | }; 92 | 93 | module.exports = { 94 | getHourlyWeather, 95 | getDailyWeather, 96 | getDailyWeatherWithClimateIconId, 97 | getOverviewWeather: async (code, date, hour, timeFormat) => { 98 | let dailyClimate; 99 | if (!hour) { 100 | hour = 12; 101 | 102 | const dailyWeatherWithClimate = await getDailyWeatherWithClimateIconId( 103 | code, 104 | date 105 | ); 106 | 107 | if (!dailyWeatherWithClimate) return null; 108 | 109 | dailyClimate = await climateService.getClimateByIconId( 110 | dailyWeatherWithClimate.climateIconId 111 | ); 112 | } 113 | const location = await locationService.getLocationByCode(code); 114 | const dailyWeather = await getDailyWeather(code, date); 115 | const hourlyWeather = await getHourlyWeather( 116 | code, 117 | date, 118 | hour, 119 | timeFormat 120 | ); 121 | 122 | if (!dailyWeather || !hourlyWeather) { 123 | return null; 124 | } 125 | 126 | if (dailyClimate) hourlyWeather.climate = dailyClimate; 127 | 128 | return { 129 | region: location, 130 | dailyWeather, 131 | hourlyWeather 132 | }; 133 | }, 134 | 135 | getOverviewWeathers: async (keyword, date, hour, timeFormat) => { 136 | let defaultClimateFlag = false; 137 | 138 | if (!hour) { 139 | hour = 12; 140 | defaultClimateFlag = true; 141 | } 142 | 143 | const locations = await locationService.getLocationsByKeyword(keyword); 144 | let overviewWeatherList = []; 145 | 146 | for (let i = 0; i < locations.length; ++i) { 147 | const location = locations[i]; 148 | const dailyWeather = await getDailyWeather( 149 | location.dataValues.code, 150 | date 151 | ); 152 | const hourlyWeather = await getHourlyWeather( 153 | location.dataValues.code, 154 | date, 155 | hour, 156 | timeFormat 157 | ); 158 | if (!dailyWeather || !hourlyWeather) { 159 | continue; 160 | } 161 | if (defaultClimateFlag) { 162 | const dailyWeatherWithClimate = await getDailyWeatherWithClimateIconId( 163 | location.dataValues.code, 164 | date 165 | ); 166 | 167 | if (!dailyWeatherWithClimate) continue; 168 | 169 | hourlyWeather.climate = await climateService.getClimateByIconId( 170 | dailyWeatherWithClimate.climateIconId 171 | ); 172 | } 173 | 174 | overviewWeatherList.push({ 175 | region: location, 176 | dailyWeather, 177 | hourlyWeather 178 | }); 179 | } 180 | return overviewWeatherList; 181 | }, 182 | 183 | getExtraDailyWeather: async (code, date) => { 184 | const dailyWeather = await DailyWeather.findOne({ 185 | where: { location_id: code, date: date } 186 | }); 187 | if (!dailyWeather) { 188 | throw Error(exception.NO_DATA); 189 | } 190 | return { 191 | rain: { 192 | value: dailyWeather.precipitation, 193 | rating: getRainRating(dailyWeather.precipitation) 194 | }, 195 | humidity: { 196 | value: dailyWeather.humidity, 197 | rating: getHumidityRating(dailyWeather.humidity) 198 | }, 199 | wind: { 200 | value: dailyWeather.wind_speed, 201 | rating: getWindRating(dailyWeather.wind_speed) 202 | } 203 | }; 204 | }, 205 | 206 | getDailyClimateId: async (code, date) => { 207 | const dailyWeather = await DailyWeather.findOne({ 208 | where: { location_id: code, date } 209 | }); 210 | if (!dailyWeather) { 211 | return null; 212 | } 213 | return { 214 | climateId: dailyWeather.climate_id 215 | }; 216 | }, 217 | 218 | getDailyWeatherId: async (code, date) => { 219 | const dailyWeather = await DailyWeather.findOne({ 220 | where: { 221 | location_id: code, 222 | date 223 | } 224 | }); 225 | 226 | if (!dailyWeather) { 227 | return null; 228 | } 229 | 230 | return dailyWeather.id; 231 | } 232 | }; 233 | -------------------------------------------------------------------------------- /WeathyServer/services/weathyService.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const { Op, literal, UniqueConstraintError } = require('sequelize'); 3 | const { format12 } = require('../utils/dateUtils'); 4 | const { 5 | Weathy, 6 | DailyWeather, 7 | sequelize, 8 | WeathyClothes, 9 | Clothes 10 | } = require('../models'); 11 | const locationService = require('./locationService'); 12 | const weatherService = require('./weatherService'); 13 | const clothesService = require('./clothesService'); 14 | const climateService = require('./climateService'); 15 | const exception = require('../modules/exception'); 16 | const { getDailyClimateId } = require('./weatherService'); 17 | 18 | const calculateConditionPoint = (candidate, todayWeather) => { 19 | const { todayTemp, todayClimateId } = todayWeather; 20 | const { minTemp: todayMinTemp, maxTemp: todayMaxTemp } = todayTemp; 21 | const { 22 | temperature_min: pastMinTemp, 23 | temperature_max: pastMaxTemp, 24 | climate_id: pastClimateId 25 | } = candidate.DailyWeather; 26 | 27 | const condition1 = 28 | (Math.abs(todayMaxTemp - pastMaxTemp) + 29 | Math.abs(todayMinTemp - pastMinTemp)) * 30 | 3; 31 | const condition2 = 32 | Math.abs( 33 | Math.abs(todayMaxTemp - todayMinTemp) - 34 | Math.abs(pastMaxTemp - pastMinTemp) 35 | ) * 2; 36 | const condition3 = todayClimateId % 100 === pastClimateId % 100 ? 1 : 0; 37 | 38 | return condition1 + condition2 + condition3; 39 | }; 40 | 41 | const getWeathyOnDate = async (date, userId) => { 42 | const weathy = await Weathy.findOne({ 43 | include: [ 44 | { 45 | model: DailyWeather, 46 | required: true, 47 | attributes: [ 48 | 'temperature_max', 49 | 'temperature_min', 50 | 'climate_id', 51 | 'location_id', 52 | 'date' 53 | ], 54 | where: { 55 | date 56 | } 57 | } 58 | ], 59 | where: { 60 | user_id: userId 61 | } 62 | }); 63 | 64 | return weathy; 65 | }; 66 | 67 | const selectBestDate = (todayWeather, candidates) => { 68 | const SUITABLE_STAMP_ID = 3; 69 | 70 | let weathy = { 71 | recommend: undefined, 72 | point: undefined 73 | }; 74 | 75 | for (let candidate of candidates) { 76 | let point = calculateConditionPoint(candidate, todayWeather); 77 | 78 | //init 79 | if (!weathy.recommend) { 80 | weathy.recommend = candidate; 81 | weathy.point = point; 82 | } 83 | 84 | if (point < weathy.point) { 85 | weathy.recommend = candidate; 86 | weathy.point = point; 87 | } else if ( 88 | weathy.point === point && 89 | candidate.emoji_id === SUITABLE_STAMP_ID 90 | ) { 91 | weathy.recommend = candidate; 92 | } 93 | } 94 | 95 | if (!weathy.recommend) return null; 96 | 97 | return weathy.recommend.DailyWeather.date; 98 | }; 99 | 100 | const getSuitableWeathers = (todayWeather, weathies) => { 101 | const { todayTemp } = todayWeather; 102 | const { minTemp: todayMinTemp, maxTemp: todayMaxTemp } = todayTemp; 103 | const weathyCase = { 104 | 1: [], 105 | 2: [] 106 | }; 107 | 108 | for (let w of weathies) { 109 | const { 110 | temperature_min: pastMinTemp, 111 | temperature_max: pastMaxTemp 112 | } = w.DailyWeather; 113 | 114 | const maxTempPoint = Math.abs(todayMaxTemp - pastMaxTemp); 115 | const minTempPoint = Math.abs(todayMinTemp - pastMinTemp); 116 | 117 | if (maxTempPoint <= 2 && minTempPoint <= 2) weathyCase[1].push(w); 118 | else if (maxTempPoint <= 2 || minTempPoint <= 2) weathyCase[2].push(w); 119 | } 120 | 121 | if (weathyCase[1].length !== 0) return weathyCase[1]; 122 | 123 | return weathyCase[2]; 124 | }; 125 | 126 | const loadWeatherOnDate = async (code, date) => { 127 | const dailyWeather = await weatherService.getDailyWeather(code, date); 128 | const dailyClimateId = await weatherService.getDailyClimateId(code, date); 129 | 130 | if (!dailyWeather || !dailyClimateId) return null; 131 | 132 | const { temperature: todayTemp } = dailyWeather; 133 | const { climateId: todayClimateId } = dailyClimateId; 134 | 135 | if (!todayTemp || !todayClimateId) return null; 136 | 137 | return { 138 | todayTemp, 139 | todayClimateId 140 | }; 141 | }; 142 | 143 | const getMostSimilarDate = async (code, date, candidates) => { 144 | const todayWeather = await loadWeatherOnDate(code, date); //현재 지역의 날씨 로드 145 | 146 | if (!todayWeather) return null; 147 | 148 | const candidatesOfSuitableCase = getSuitableWeathers( 149 | todayWeather, 150 | candidates 151 | ); //적합한 case의 weathers 가져옴 152 | 153 | return selectBestDate(todayWeather, candidatesOfSuitableCase); 154 | }; 155 | 156 | const upsertWeathyClothes = async (clothes, weathyId, transaction) => { 157 | const clothesDBForm = []; 158 | 159 | await WeathyClothes.destroy( 160 | { 161 | where: { 162 | weathy_id: weathyId 163 | } 164 | }, 165 | { transaction } 166 | ); 167 | 168 | for (let c of clothes) { 169 | clothesDBForm.push({ 170 | weathy_id: weathyId, 171 | 172 | clothes_id: c 173 | }); 174 | } 175 | 176 | await WeathyClothes.bulkCreate(clothesDBForm, { transaction }); 177 | }; 178 | 179 | const loadWeathiesInSixtyDays = async (date, userId) => { 180 | const sixtyAgo = dayjs(date).subtract(60, 'day').format('YYYY-MM-DD'); 181 | 182 | const weathies = await Weathy.findAll({ 183 | include: [ 184 | { 185 | model: DailyWeather, 186 | required: true, 187 | attributes: [ 188 | 'temperature_max', 189 | 'temperature_min', 190 | 'climate_id', 191 | 'location_id', 192 | 'date' 193 | ], 194 | where: { 195 | date: { 196 | [Op.lt]: date, 197 | [Op.gte]: sixtyAgo 198 | } 199 | } 200 | } 201 | ], 202 | where: { 203 | user_id: userId 204 | }, 205 | 206 | order: literal('DailyWeather.date ASC') 207 | }); 208 | 209 | return weathies; 210 | }; 211 | 212 | const getRecommendedWeathy = async (code, date, userId) => { 213 | const candidates = await loadWeathiesInSixtyDays(date, userId); 214 | const mostSimilarDate = await getMostSimilarDate(code, date, candidates); 215 | 216 | if (!mostSimilarDate) return null; 217 | 218 | const recommendedWeathy = await getWeathy(mostSimilarDate, userId); 219 | 220 | return recommendedWeathy; 221 | }; 222 | 223 | const checkOwnerClothes = async (clothes, userId) => { 224 | const clothesIdSet = new Set(); 225 | 226 | const clothesList = await Clothes.findAll({ 227 | where: { 228 | user_id: userId 229 | }, 230 | attributes: ['id'] 231 | }); 232 | 233 | for (let c of clothesList) { 234 | clothesIdSet.add(c.id); 235 | } 236 | for (let c of clothes) { 237 | if (!clothesIdSet.has(c)) return false; 238 | } 239 | 240 | return true; 241 | }; 242 | 243 | const findDailyWeatherByWeathy = async ( 244 | weathyId, 245 | code, 246 | userId, 247 | transaction 248 | ) => { 249 | const target = await Weathy.findOne( 250 | { 251 | include: [ 252 | { 253 | model: DailyWeather, 254 | required: true, 255 | attributes: ['id', 'date'] 256 | } 257 | ], 258 | where: { 259 | id: weathyId, 260 | user_id: userId 261 | } 262 | }, 263 | { transaction } 264 | ); 265 | 266 | if (!target) return null; 267 | 268 | const dailyWeatherDate = target.DailyWeather.date; 269 | const dailyWeather = await DailyWeather.findOne( 270 | { 271 | where: { 272 | date: dailyWeatherDate, 273 | location_id: code 274 | } 275 | }, 276 | { transaction } 277 | ); 278 | 279 | return dailyWeather; 280 | }; 281 | 282 | const getWeathy = async (date, userId) => { 283 | const weathy = await getWeathyOnDate(date, userId); 284 | 285 | if (!weathy) return null; 286 | 287 | const { location_id: code } = weathy.DailyWeather; 288 | const dailyWeather = await weatherService.getDailyWeather(code, date); 289 | const hourlyWeather = await weatherService.getHourlyWeather( 290 | code, 291 | date, 292 | 12, 293 | format12 294 | ); 295 | 296 | if (!hourlyWeather) return null; 297 | 298 | // Hotfix. Should be updated in the future. 299 | const dailyClimate = await getDailyClimateId(code, date); 300 | hourlyWeather.climate.iconId = dailyClimate.climateId; 301 | hourlyWeather.climate = await climateService.getClimateByIconId( 302 | hourlyWeather.climate.iconId 303 | ); 304 | 305 | const region = await locationService.getLocationByCode(code); 306 | const closet = await clothesService.getWeathyCloset(weathy.id); 307 | 308 | return { 309 | weathy: { 310 | region, 311 | dailyWeather, 312 | hourlyWeather, 313 | closet, 314 | weathyId: weathy.id, 315 | stampId: weathy.emoji_id, 316 | feedback: weathy.description || null, 317 | imgUrl: weathy.img_url || null 318 | } 319 | }; 320 | }; 321 | 322 | const createWeathy = async ( 323 | dailyWeatherId, 324 | clothes, 325 | stampId, 326 | userId, 327 | feedback = null, 328 | imgUrl = null 329 | ) => { 330 | const transaction = await sequelize.transaction(); 331 | 332 | try { 333 | const weathy = await Weathy.create( 334 | { 335 | user_id: userId, 336 | dailyweather_id: dailyWeatherId, 337 | emoji_id: stampId, 338 | description: feedback, 339 | img_url: imgUrl 340 | }, 341 | { transaction } 342 | ); 343 | 344 | await upsertWeathyClothes(clothes, weathy.id, transaction); 345 | 346 | await transaction.commit(); 347 | return weathy.id; 348 | } catch (err) { 349 | await transaction.rollback(); 350 | 351 | if (err instanceof UniqueConstraintError) { 352 | throw Error(exception.DUPLICATION_WEATHY); 353 | } 354 | 355 | throw Error(exception.SERVER_ERROR); 356 | } 357 | }; 358 | 359 | const deleteWeathy = async (weathyId, userId) => { 360 | try { 361 | const deletedWeathy = await Weathy.destroy({ 362 | where: { 363 | user_id: userId, 364 | id: weathyId 365 | } 366 | }); 367 | 368 | return deletedWeathy; 369 | } catch (err) { 370 | throw Error(exception.SERVER_ERROR); 371 | } 372 | }; 373 | 374 | const modifyWeathy = async ( 375 | weathyId, 376 | userId, 377 | code, 378 | clothes, 379 | stampId, 380 | feedback = null 381 | ) => { 382 | const transaction = await sequelize.transaction(); 383 | 384 | try { 385 | const dailyWeather = await findDailyWeatherByWeathy( 386 | weathyId, 387 | code, 388 | userId, 389 | transaction 390 | ); 391 | 392 | if (!dailyWeather) throw Error(exception.NO_DAILY_WEATHER); 393 | 394 | const isUpdated = await Weathy.update( 395 | { 396 | dailyweather_id: dailyWeather.id, 397 | emoji_id: stampId, 398 | description: feedback 399 | }, 400 | { 401 | where: { 402 | user_id: userId, 403 | id: weathyId 404 | } 405 | }, 406 | { transaction } 407 | ); 408 | 409 | await upsertWeathyClothes(clothes, weathyId, transaction); 410 | 411 | await transaction.commit(); 412 | 413 | return isUpdated; 414 | } catch (err) { 415 | await transaction.rollback(); 416 | 417 | if (err.message === exception.NO_DAILY_WEATHER) { 418 | throw Error(exception.NO_DAILY_WEATHER); 419 | } 420 | if (err instanceof UniqueConstraintError) { 421 | throw Error(exception.DUPLICATION_WEATHY); 422 | } 423 | throw Error(exception.SERVER_ERROR); 424 | } 425 | }; 426 | 427 | const modifyImgField = async (imgUrl = null, weathyId, userId) => { 428 | try { 429 | await Weathy.update( 430 | { 431 | img_url: imgUrl 432 | }, 433 | { 434 | where: { 435 | user_id: userId, 436 | id: weathyId 437 | } 438 | } 439 | ); 440 | } catch (err) { 441 | throw Error(exception.SERVER_ERROR); 442 | } 443 | }; 444 | 445 | const isDuplicateWeathy = async (dailyWeatherId, userId) => { 446 | try { 447 | const count = await Weathy.count({ 448 | where: { 449 | user_id: userId, 450 | dailyweather_id: dailyWeatherId 451 | } 452 | }); 453 | if (count) return true; 454 | return false; 455 | } catch (err) { 456 | throw Error(exception.SERVER_ERROR); 457 | } 458 | }; 459 | 460 | module.exports = { 461 | getRecommendedWeathy, 462 | getWeathy, 463 | createWeathy, 464 | deleteWeathy, 465 | modifyWeathy, 466 | checkOwnerClothes, 467 | modifyImgField, 468 | isDuplicateWeathy 469 | }; 470 | -------------------------------------------------------------------------------- /WeathyServer/tests/modules/logger.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const decache = require('decache'); 3 | const { transports } = require('winston'); 4 | let logger = require('../../modules/logger'); 5 | 6 | describe('logger test', function () { 7 | let prev_node_env = process.env.NODE_ENV; 8 | beforeEach('init cache', () => { 9 | decache('../../modules/logger'); 10 | logger = require('../../modules/logger'); 11 | }); 12 | describe('production mode test', () => { 13 | before('set NODE_ENV to production', () => { 14 | process.env.NODE_ENV = 'production'; 15 | }); 16 | after('reset NODE_ENV', () => { 17 | if (!prev_node_env) { 18 | delete process.env.NODE_ENV; 19 | } else { 20 | process.env.NODE_ENV = prev_node_env; 21 | } 22 | }); 23 | it('production mode not logs to console', () => { 24 | for (let i = 0; i < logger.transports.length; ++i) { 25 | assert.ok( 26 | !(logger.transports[i] instanceof transports.Console) 27 | ); 28 | } 29 | }); 30 | }); 31 | describe('development mode test', () => { 32 | before('set NODE_ENV to development', () => { 33 | process.env.NODE_ENV = 'development'; 34 | }); 35 | after('reset NODE_ENV', () => { 36 | if (!prev_node_env) { 37 | delete process.env.NODE_ENV; 38 | } else { 39 | process.env.NODE_ENV = prev_node_env; 40 | } 41 | }); 42 | it('development mode logs to console', () => { 43 | let cnt_console_logger = 0; 44 | for (let i = 0; i < logger.transports.length; ++i) { 45 | if (logger.transports[i] instanceof transports.Console) { 46 | cnt_console_logger += 1; 47 | } 48 | } 49 | assert.ok(cnt_console_logger > 0); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /WeathyServer/tests/modules/tokenMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const httpMocks = require('node-mocks-http'); 3 | const dayjs = require('dayjs'); 4 | const { Token } = require('../../models'); 5 | const { validateToken, updateToken } = require('../../modules/tokenMiddleware'); 6 | 7 | describe('tokenMiddleware test', function () { 8 | describe('validateToken test', () => { 9 | // testing tokens:: 10 | // userID 22 (Should be Updated) 11 | // userID 25 (Should not be Updated) 12 | 13 | const firstTestUserId = 22; 14 | const secondTestUserId = 25; 15 | let firstTestUserToken, secondTestUserToken; 16 | 17 | before('get values, and update tokens used for test', async () => { 18 | const firstTestUserTokenObj = await Token.findOne({ 19 | where: { user_id: firstTestUserId } 20 | }); 21 | firstTestUserToken = firstTestUserTokenObj.token; 22 | const secondTestUserTokenObj = await Token.findOne({ 23 | where: { user_id: secondTestUserId } 24 | }); 25 | secondTestUserToken = secondTestUserTokenObj.token; 26 | 27 | const req = httpMocks.createRequest(); 28 | const res = httpMocks.createResponse({ 29 | locals: { 30 | tokenValue: firstTestUserToken 31 | } 32 | }); 33 | await updateToken(req, res); 34 | }); 35 | 36 | it('If there is no token on req, throw error', async () => { 37 | const req = httpMocks.createRequest(); 38 | const res = httpMocks.createResponse(); 39 | 40 | await validateToken(req, res, (next) => { 41 | assert.ok(next instanceof Error); 42 | }); 43 | }); 44 | 45 | it('If there is no matching token on DB, throw error', async () => { 46 | const req = httpMocks.createRequest({ 47 | headers: { 48 | 'x-access-token': 'THIS_IS_FAKE_TOKEN' 49 | } 50 | }); 51 | const res = httpMocks.createResponse(); 52 | 53 | await validateToken(req, res, (next) => { 54 | assert.ok(next instanceof Error); 55 | }); 56 | }); 57 | 58 | it("If param's userId and token's userId is different, throw error", async () => { 59 | const req = httpMocks.createRequest({ 60 | headers: { 61 | 'x-access-token': firstTestUserToken 62 | }, 63 | params: { 64 | userId: secondTestUserId 65 | } 66 | }); 67 | const res = httpMocks.createResponse(); 68 | 69 | await validateToken(req, res, (next) => { 70 | assert.ok(next instanceof Error); 71 | }); 72 | }); 73 | 74 | it('If token is expired, throw error', async () => { 75 | const req = httpMocks.createRequest({ 76 | headers: { 77 | 'x-access-token': secondTestUserToken 78 | } 79 | }); 80 | const res = httpMocks.createResponse(); 81 | 82 | await validateToken(req, res, (next) => { 83 | assert.ok(next instanceof Error); 84 | }); 85 | }); 86 | 87 | it('If token is valid, does not throw error (No param)', async () => { 88 | const req = httpMocks.createRequest({ 89 | headers: { 90 | 'x-access-token': firstTestUserToken 91 | } 92 | }); 93 | const res = httpMocks.createResponse(); 94 | 95 | await validateToken(req, res, (next) => { 96 | assert.ok(!(next instanceof Error)); 97 | }); 98 | }); 99 | 100 | it('If token is valid, does not throw error (Has param)', async () => { 101 | const req = httpMocks.createRequest({ 102 | headers: { 103 | 'x-access-token': firstTestUserToken 104 | }, 105 | params: { 106 | userId: firstTestUserId 107 | } 108 | }); 109 | const res = httpMocks.createResponse(); 110 | 111 | await validateToken(req, res, (next) => { 112 | assert.ok(!(next instanceof Error)); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('updateToken test', () => { 118 | // testing tokens:: 119 | // userID 22 (Gets Updated) 120 | const testUserId = 22; 121 | let testUserToken; 122 | let beforeUpdatedTime; 123 | 124 | before('get values used for test', async () => { 125 | const testUserTokenObj = await Token.findOne({ 126 | where: { user_id: testUserId } 127 | }); 128 | testUserToken = testUserTokenObj.token; 129 | beforeUpdatedTime = dayjs(testUserTokenObj.updated_at); 130 | }); 131 | 132 | it('Token should be updated', async () => { 133 | const req = httpMocks.createRequest(); 134 | const res = httpMocks.createResponse({ 135 | locals: { 136 | tokenValue: testUserToken 137 | } 138 | }); 139 | await updateToken(req, res); 140 | 141 | const afterTestUserTokenObj = await Token.findOne({ 142 | where: { user_id: testUserId } 143 | }); 144 | const afterUpdatedTime = dayjs(afterTestUserTokenObj.updated_at); 145 | 146 | assert.ok(afterUpdatedTime.isAfter(beforeUpdatedTime)); 147 | assert.ok(beforeUpdatedTime.isBefore(afterUpdatedTime)); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/calendarService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { calendarService } = require('../../services'); 3 | 4 | describe('calendarService test', function () { 5 | describe('getValidCalendarOverviewList test', function () { 6 | it('getValidCalendarOverviewList returns CalendarOverviewList', async function () { 7 | const validCalendarOverviews = calendarService.getValidCalendarOverviewList( 8 | 1, 9 | '2021-01-01', 10 | '2021-01-02' 11 | ); 12 | assert.ok((await validCalendarOverviews).length, 2); 13 | }); 14 | 15 | it('getValidCalendarOverviewList returns CalendarOverviewList', async function () { 16 | const validCalendarOverviews = calendarService.getValidCalendarOverviewList( 17 | 1, 18 | '2021-01-01', 19 | '2021-01-08' 20 | ); 21 | assert.ok((await validCalendarOverviews).length, 2); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/climateService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { climateService } = require('../../services'); 3 | 4 | describe('climate service test', function () { 5 | describe('getClimate test', function () { 6 | it('getClimateBy returns Climate test', async function () { 7 | const climate = await climateService.getClimate(1, 0); 8 | assert.ok(climate); 9 | assert.strictEqual(climate.iconId, 1); 10 | }); 11 | it('getClimateBy returns different description every time', async function () { 12 | const climate_first = await climateService.getClimate(1, 0); 13 | const climate_second = await climateService.getClimate(1, 0); 14 | assert.ok(climate_first.description != climate_second.description); 15 | }); 16 | it('getClimateBy throws error if not exists', async function () { 17 | await assert.rejects(async () => { 18 | await climateService.getClimate(-2, 0); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/clothesService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { Clothes, Weathy } = require('../../models'); 3 | const { 4 | getClothesByUserId, 5 | addClothesByUserId, 6 | deleteClothesByUserId, 7 | getClothesNumByUserId, 8 | getClothesByWeathyId 9 | } = require('../../services/clothesService'); 10 | const { createWeathy, deleteWeathy } = require('../../services/weathyService'); 11 | 12 | describe('clothesService test', function () { 13 | describe('getClothesNumByUserId test', () => { 14 | // uses userId = 450 (Has only original clothes: 12 clothes) 15 | const userId = 450; 16 | it('getClothesNumByUserId returns clothesNum', async () => { 17 | const firstClothesNum = await getClothesNumByUserId(userId); 18 | assert.strictEqual(firstClothesNum, 12); 19 | 20 | // add one clothes for test 21 | const testClothesName = 'Test_Cl'; 22 | await addClothesByUserId(userId, 1, testClothesName); 23 | const secondClothesNum = await getClothesNumByUserId(userId); 24 | assert.strictEqual(secondClothesNum, 13); 25 | 26 | // delete the clothes 27 | const testClothes = await Clothes.findOne({ 28 | where: { 29 | user_id: userId, 30 | name: testClothesName 31 | } 32 | }); 33 | const clothesList = new Array(); 34 | clothesList.push(testClothes.id); 35 | await deleteClothesByUserId(userId, clothesList); 36 | const thirdClothesNum = await getClothesNumByUserId(userId); 37 | assert.strictEqual(thirdClothesNum, 12); 38 | }); 39 | }); 40 | 41 | describe('getClothesByUserId test', () => { 42 | let closet, userId; 43 | before('get closet', async () => { 44 | userId = 35; 45 | closet = await getClothesByUserId(userId); 46 | }); 47 | it('getClothesByUserId returns closet', async () => { 48 | assert.strictEqual(closet.bottom.categoryId, 2); 49 | assert.strictEqual(closet.bottom.clothes[0].id, 16); 50 | assert.strictEqual(closet.bottom.clothes[0].name, '바지1'); 51 | }); 52 | }); 53 | 54 | describe('getClothesByWeathyID test', () => { 55 | const userId = 450; 56 | const testClothesName = 'testC'; 57 | let testWeathyId, testClothesId; 58 | before('Add Clothes -> Record Weathy -> Delete Clothes', async () => { 59 | await addClothesByUserId(userId, 1, testClothesName); 60 | const testClothes = await Clothes.findOne({ 61 | where: { 62 | user_id: userId, 63 | name: testClothesName 64 | } 65 | }); 66 | testClothesId = testClothes.id; 67 | const recordedClothesList = [5035, 5040, 5043]; 68 | recordedClothesList.push(testClothesId); 69 | 70 | await createWeathy(1, recordedClothesList, 3, userId, 'TEST'); 71 | 72 | const testWeathy = await Weathy.findOne({ 73 | where: { 74 | user_id: userId, 75 | dailyWeather_id: 1 76 | } 77 | }); 78 | testWeathyId = testWeathy.id; 79 | 80 | const testClothesList = new Array(); 81 | testClothesList.push(testClothesId); 82 | 83 | await deleteClothesByUserId(userId, testClothesList); 84 | }); 85 | 86 | after('Delete Clothes and Weathy', async () => { 87 | await deleteWeathy(testWeathyId, userId); 88 | }); 89 | 90 | it('getClothesByWeathyId should store clothes deleted after record', async () => { 91 | const closet = await getClothesByWeathyId(userId, testWeathyId); 92 | assert.strictEqual(closet.top.clothes[0].id, testClothesId); 93 | assert.strictEqual(closet.top.clothes[0].name, testClothesName); 94 | }); 95 | }); 96 | 97 | let userId, category, number, name; 98 | userId = 35; 99 | category = 1; 100 | 101 | describe('addClothesByUserId test', () => { 102 | it('First addition of clothes', async () => { 103 | number = Math.floor(Math.random() * 100000); 104 | name = '옷' + number; 105 | 106 | await addClothesByUserId(userId, category, name); 107 | const addedClothes = await Clothes.findOne({ 108 | where: { category_id: category, name: name } 109 | }); 110 | 111 | assert.ok(addedClothes !== null); 112 | }); 113 | 114 | it('Second addition makes exception ALREADY_CLOTHES', async () => { 115 | await assert.rejects(async () => { 116 | await addClothesByUserId(userId, category, name); 117 | }); 118 | }); 119 | }); 120 | 121 | let clothes = new Array(); 122 | describe('deleteClothesByUserId test', () => { 123 | it('First deletion of clothes', async () => { 124 | const deletedBeforeClothes = await Clothes.findOne({ 125 | where: { user_id: userId, category_id: category, name: name } 126 | }); 127 | const deletedId = deletedBeforeClothes.id; 128 | clothes.push(deletedId); 129 | await deleteClothesByUserId(userId, clothes); 130 | 131 | const deletedAfterClothes = await Clothes.findOne({ 132 | where: { 133 | user_id: userId, 134 | category_id: category, 135 | name: name, 136 | is_deleted: 0 137 | } 138 | }); 139 | 140 | assert.strictEqual(deletedAfterClothes, null); 141 | }).timeout(15000); 142 | 143 | it('Second deletion makes exception NO_CLOTHES', async () => { 144 | await assert.rejects(async () => { 145 | await deleteClothesByUserId(userId, clothes); 146 | }); 147 | }); 148 | 149 | it("When deleting other user's clothes, throws error", async () => { 150 | const wrongUserId = userId + 1; 151 | 152 | await assert.rejects(async () => { 153 | await deleteClothesByUserId(wrongUserId, clothes); 154 | }); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/locationService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { locationService } = require('../../services'); 3 | const exception = require('../../modules/exception'); 4 | 5 | describe('location service test', function () { 6 | describe('getLocationByCode test', function () { 7 | it('getLocationById returns location', async function () { 8 | const location = await locationService.getLocationByCode( 9 | 1100000000 10 | ); 11 | assert.strictEqual(location.code, 1100000000); 12 | assert.strictEqual(location.name, '서울특별시'); 13 | }); 14 | it('getLocationById throws error if not exists', async function () { 15 | await assert.rejects(async () => { 16 | await locationService.getById(0); 17 | }, exception.NO_DATA); 18 | }); 19 | }); 20 | describe('getLocationsByKeyword', function () { 21 | it('getLocationsByKeyword returns locations', async function () { 22 | const locations = await locationService.getLocationsByKeyword( 23 | '서울' 24 | ); 25 | assert.strictEqual(locations.length, 26); 26 | }); 27 | it('getLocationsByKeyword returns empty array if not exists', async function () { 28 | const locations = await locationService.getLocationsByKeyword( 29 | '김자현' 30 | ); 31 | assert.strictEqual(locations.length, 0); 32 | }); 33 | }); 34 | describe('getCode test', function () { 35 | it('getCode returns code', async function () { 36 | const code = await locationService.getCode( 37 | 37.57037778, 38 | 126.98164166666668 39 | ); 40 | assert.strictEqual(code, 1111000000); 41 | }); 42 | it('getCode throws error if not exists', async function () { 43 | await assert.ok(async () => { 44 | await locationService.getCode(-1, -1); 45 | }, exception.SERVER_ERROR); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/tokenService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { Token } = require('../../models'); 3 | const { createTokenOfUser } = require('../../services/tokenService'); 4 | 5 | describe('tokenService test', function () { 6 | describe('createTokenOfUser test', () => { 7 | const testUserId = 1; 8 | it('token should be created by user_id', async () => { 9 | await createTokenOfUser(testUserId); 10 | const token = await Token.findOne({ 11 | where: { user_id: testUserId } 12 | }); 13 | assert.ok(token !== null); 14 | }); 15 | 16 | after('Delete created token', async () => { 17 | await Token.destroy({ 18 | where: { user_id: testUserId } 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/userService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { User, Clothes } = require('../../models'); 3 | const { 4 | getUserByAccount, 5 | createUserByUuid, 6 | modifyUserById 7 | } = require('../../services/userService'); 8 | 9 | describe('userService test', function () { 10 | describe('getUserByAccount Test', () => { 11 | it('verify that user is obtained by uuid correctly', async () => { 12 | const uuid = 'test'; 13 | const firstUser = await getUserByAccount(uuid); 14 | const secondUser = await User.findOne({ where: { id: 1 } }); 15 | assert.ok(firstUser.id === secondUser.id); 16 | assert.ok(firstUser.nickname === secondUser.nickname); 17 | }); 18 | 19 | it('if there is no user, exception NO_USER', async () => { 20 | const uuid = 'thisisnotuser'; 21 | await assert.rejects(async () => { 22 | await getUserByAccount(uuid); 23 | }); 24 | }); 25 | }); 26 | 27 | describe('createUserByUuid Test', () => { 28 | it('user should be correctly created', async () => { 29 | const number = Math.floor(Math.random() * 100000); 30 | const uuid = 'Test' + number; 31 | const nickname = 'Test'; 32 | const firstUser = await createUserByUuid(uuid, nickname); 33 | const secondUser = await User.findOne({ order: [['id', 'DESC']] }); 34 | assert.ok(firstUser.id === secondUser.id); 35 | assert.ok(firstUser.nickname === secondUser.nickname); 36 | const clothes = await Clothes.findOne({ 37 | where: { user_id: firstUser.id, name: '티셔츠' } 38 | }); 39 | assert.ok(clothes !== null); 40 | }); 41 | 42 | it('if already uuid exists, exception ALRLEADY_USER', async () => { 43 | const uuid = 'test'; 44 | const nickname = 'usertest'; 45 | 46 | await assert.rejects(async () => { 47 | await createUserByUuid(uuid, nickname); 48 | }); 49 | }); 50 | }); 51 | 52 | let originalNickname; 53 | describe('modifyUserById Test', () => { 54 | before('save original nickname', async () => { 55 | const user = await User.findOne({ where: { id: 1 } }); 56 | originalNickname = user.nickname; 57 | }); 58 | 59 | after('put nickname to original', async () => { 60 | await User.update( 61 | { nickname: originalNickname }, 62 | { where: { id: 1 } } 63 | ); 64 | }); 65 | 66 | it('user nickname should be changed', async () => { 67 | const userId = 1; 68 | const nickname = 'yeonsang'; 69 | await modifyUserById(userId, nickname); 70 | const user = await User.findOne({ where: { id: 1 } }); 71 | 72 | assert.ok(user.nickname === 'yeonsang'); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/weatherService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const dateUtils = require('../../utils/dateUtils'); 3 | const { weatherService } = require('../../services'); 4 | 5 | const assertDailyWeather = (dailyWeather) => { 6 | assert.strictEqual(dailyWeather.date.month, 1); 7 | assert.strictEqual(dailyWeather.date.day, 1); 8 | assert.strictEqual(dailyWeather.date.dayOfWeek, '금요일'); 9 | assert.strictEqual(dailyWeather.temperature.maxTemp, -100); 10 | assert.strictEqual(dailyWeather.temperature.minTemp, 100); 11 | }; 12 | 13 | const assertDailyWeatherWithClimateIconId = (dailyWeather) => { 14 | assert.strictEqual(dailyWeather.date.month, 1); 15 | assert.strictEqual(dailyWeather.date.day, 1); 16 | assert.strictEqual(dailyWeather.date.dayOfWeek, '금요일'); 17 | assert.strictEqual(dailyWeather.temperature.maxTemp, -100); 18 | assert.strictEqual(dailyWeather.temperature.minTemp, 100); 19 | assert.strictEqual(dailyWeather.climateIconId, 1); 20 | }; 21 | 22 | const assertHourlyWeather = (hourlyWeather) => { 23 | assert.strictEqual(hourlyWeather.time, '오후 12시'); 24 | assert.strictEqual(hourlyWeather.temperature, 10); 25 | assert.strictEqual(hourlyWeather.climate.iconId, 2); 26 | assert.strictEqual(hourlyWeather.pop, 0); 27 | }; 28 | 29 | describe('weather service test', function () { 30 | describe('getDailyWeather test', function () { 31 | it('getDailyWeather returns dailyWeather', async function () { 32 | const dailyWeather = await weatherService.getDailyWeather( 33 | 1100000000, 34 | '2021-01-01' 35 | ); 36 | assertDailyWeather(dailyWeather); 37 | }); 38 | it('getDailyWeather returns null if not exists', async function () { 39 | const dailyWeather = await weatherService.getDailyWeather( 40 | 1100000000, 41 | '2020-01-01' 42 | ); 43 | assert(dailyWeather == null); 44 | }); 45 | }); 46 | describe('getDailyWeatherWithClimateIconId test', function () { 47 | it('getDailyWeatherWithClimateIconId returns dailyWeatherWithClimate', async function () { 48 | const dailyWeather = await weatherService.getDailyWeatherWithClimateIconId( 49 | 1100000000, 50 | '2021-01-01' 51 | ); 52 | assertDailyWeatherWithClimateIconId(dailyWeather); 53 | }); 54 | it('getDailyWeatherWithClimateIconId returns null if not exists', async function () { 55 | const dailyWeather = await weatherService.getDailyWeatherWithClimateIconId( 56 | 1100000000, 57 | '2020-01-01' 58 | ); 59 | assert(dailyWeather == null); 60 | }); 61 | }); 62 | describe('getHourlyWeather test', function () { 63 | it('getHourlyWeather returns hourlyWeather', async function () { 64 | const hourlyWeather = await weatherService.getHourlyWeather( 65 | 1100000000, 66 | '2021-01-01', 67 | 12, 68 | dateUtils.format12 69 | ); 70 | assertHourlyWeather(hourlyWeather); 71 | }); 72 | it('getHourlyWeather returns null if not exists', async function () { 73 | const dailyWeather = await weatherService.getDailyWeather( 74 | 1100000000, 75 | '2020-01-01' 76 | ); 77 | assert(dailyWeather == null); 78 | }); 79 | }); 80 | describe('getOverviewWeather test', function () { 81 | it('getOverviewWeather returns overviewWeather', async function () { 82 | const overviewWeather = await weatherService.getOverviewWeather( 83 | 1100000000, 84 | '2021-01-01', 85 | 12, 86 | dateUtils.format12 87 | ); 88 | assert.strictEqual(overviewWeather.region.code, 1100000000); 89 | assert.strictEqual(overviewWeather.region.name, '서울특별시'); 90 | assertDailyWeather(overviewWeather.dailyWeather); 91 | assertHourlyWeather(overviewWeather.hourlyWeather); 92 | }); 93 | it('getOverviewWeather returns null if not exists', async function () { 94 | const overviewWeather = await weatherService.getOverviewWeather( 95 | 1100000000, 96 | '2020-01-01', 97 | 12, 98 | dateUtils.format12 99 | ); 100 | assert(overviewWeather == null); 101 | }); 102 | }); 103 | describe('getOverviewWeathers test', function () { 104 | it('getOverviewWeathers returns overviewWeatherList', async function () { 105 | const overviewWeatherList = await weatherService.getOverviewWeathers( 106 | '서울특별시', 107 | '2021-01-01', 108 | 12, 109 | dateUtils.format12 110 | ); 111 | assert.strictEqual(overviewWeatherList.length, 1); 112 | assertDailyWeather(overviewWeatherList[0].dailyWeather); 113 | assertHourlyWeather(overviewWeatherList[0].hourlyWeather); 114 | }).timeout(15000); 115 | it('getOverviewWeathers returns empty list if not exists', async function () { 116 | const overviewWeatherList = await weatherService.getOverviewWeathers( 117 | '김자현', 118 | '2021-01-01', 119 | 12, 120 | dateUtils.format12 121 | ); 122 | assert.strictEqual(overviewWeatherList.length, 0); 123 | }); 124 | }); 125 | describe('getExtraDailyWeather test', function () { 126 | it('getExtraDailyWeather returns extraWeather', async function () { 127 | const extraWeather = await weatherService.getExtraDailyWeather( 128 | 1100000000, 129 | '2021-01-01' 130 | ); 131 | assert(extraWeather.rain.value == 10); 132 | assert(extraWeather.humidity.value == 10); 133 | assert(extraWeather.wind.value == 91.9); 134 | }); 135 | it('getExtraDailyWeather throw error if not exists', async function () { 136 | await assert.rejects(async () => { 137 | await weatherService.getExtraDailyWeather(-2, '2020-01-01'); 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /WeathyServer/tests/services/weathyService.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const weathyService = require('../../services/weathyService'); 3 | const exception = require('../../modules/exception'); 4 | const { Weathy, WeathyClothes, DailyWeather } = require('../../models'); 5 | const { sequelize } = require('../../models'); 6 | const assertRegion = (region) => { 7 | assert.strictEqual(region.code, 1100000000); 8 | assert.strictEqual(region.name, '서울특별시'); 9 | }; 10 | 11 | const assertDailyWeather = (dailyWeather) => { 12 | assert.strictEqual(dailyWeather.date.month, 1); 13 | assert.strictEqual(dailyWeather.date.day, 1); 14 | assert.strictEqual(dailyWeather.date.dayOfWeek, '금요일'); 15 | 16 | assert.strictEqual(dailyWeather.temperature.maxTemp, -100); 17 | assert.strictEqual(dailyWeather.temperature.minTemp, 100); 18 | }; 19 | 20 | const assertHourlyWeather = (hourlyWeather) => { 21 | assert.strictEqual(hourlyWeather.time, '오후 12시'); 22 | assert.strictEqual(typeof hourlyWeather.temperature, 'number'); 23 | // assert.strictEqual(hourlyWeather.climate.iconId, 2); dailyClimateIconId 24 | assert.strictEqual(hourlyWeather.pop, 0); 25 | }; 26 | 27 | const assertClosetWeather = (closet) => { 28 | assert.strictEqual(closet.top.categoryId, 1); 29 | assert.strictEqual(closet.bottom.categoryId, 2); 30 | assert.strictEqual(closet.outer.categoryId, 3); 31 | assert.strictEqual(closet.etc.categoryId, 4); 32 | }; 33 | 34 | const assertWeathy = ({ weathy }) => { 35 | assertRegion(weathy.region); 36 | assertDailyWeather(weathy.dailyWeather); 37 | assertHourlyWeather(weathy.hourlyWeather); 38 | assertClosetWeather(weathy.closet); 39 | 40 | assert.strictEqual(weathy.weathyId, 32); 41 | assert.strictEqual(weathy.stampId, 1); 42 | 43 | assert.ok(weathy.feedback === null || typeof weathy.feedback === 'string'); 44 | assert.ok(weathy.imgUrl === null || typeof weathy.imgUrl === 'string'); 45 | }; 46 | 47 | describe('weathy service test', function () { 48 | describe('getdWeathy test', function () { 49 | it('getWeathy returns Weathy', async function () { 50 | const weathy = await weathyService.getWeathy('2021-01-01', 1); 51 | assertWeathy(weathy); 52 | }); 53 | 54 | it('getDailyWeather returns null', async function () { 55 | const weathy = await weathyService.getWeathy('3000-01-01', 1); 56 | assert(weathy === null); 57 | }); 58 | }); 59 | describe('getWeathy test', function () { 60 | it('getRecommendedWeathy returns recommened weathy', async function () { 61 | const weathy = await weathyService.getRecommendedWeathy( 62 | 2647000000, 63 | '2021-01-11', 64 | 1 65 | ); 66 | const answerWeathy = await weathyService.getWeathy('2021-01-10', 1); 67 | 68 | assert.ok(weathy.id == answerWeathy.id); 69 | }); 70 | 71 | it('getRecommendedWeathy returns null', async function () { 72 | const weathy = await weathyService.getRecommendedWeathy( 73 | 1100000000, 74 | '3000-01-01', 75 | 1 76 | ); 77 | 78 | assert.ok(weathy === null); 79 | }); 80 | }); 81 | describe('createWeathy test', function () { 82 | it('createWeathy Success case test ', async function () { 83 | const weathyId = await weathyService.createWeathy( 84 | 200, 85 | [5, 6], 86 | 3, 87 | 1 88 | ); 89 | const answerWeathy = await Weathy.findOne({ 90 | where: { 91 | id: weathyId 92 | } 93 | }); 94 | 95 | assert.ok(answerWeathy.id === weathyId); 96 | 97 | await Weathy.destroy({ 98 | where: { 99 | id: weathyId 100 | }, 101 | paranoid: false 102 | }); 103 | }); 104 | it('createWeathy Duplicate Fail case test ', async function () { 105 | let weathyId; 106 | try { 107 | weathyId = await weathyService.createWeathy(200, [5, 6], 3, 1); 108 | await weathyService.createWeathy(200, [5, 6], 3, 1); 109 | assert.fail(); 110 | } catch (err) { 111 | assert.ok(err.message === exception.DUPLICATION_WEATHY); 112 | } finally { 113 | await Weathy.destroy({ 114 | where: { 115 | id: weathyId 116 | }, 117 | paranoid: false 118 | }); 119 | } 120 | }); 121 | }); 122 | 123 | describe('deleteWeathy test', function () { 124 | it('deleteWeathy Success case test ', async function () { 125 | const userId = 1; 126 | const weathyId = await weathyService.createWeathy( 127 | 200, 128 | [5, 6], 129 | 3, 130 | userId 131 | ); 132 | 133 | await weathyService.deleteWeathy(weathyId, userId); 134 | 135 | const weathy = await Weathy.findOne({ 136 | where: { 137 | id: weathyId, 138 | user_id: userId 139 | } 140 | }); 141 | const result = await WeathyClothes.findAndCountAll({ 142 | where: { 143 | weathy_id: weathyId 144 | } 145 | }); 146 | 147 | assert.ok(weathy === null); 148 | assert.ok(result.count === 0); 149 | }); 150 | }); 151 | 152 | describe('modifyWeathy test', function () { 153 | const userId = 1; 154 | const code = 1111000000; 155 | let weathyId; 156 | 157 | before('Create Weathy', async () => { 158 | weathyId = await weathyService.createWeathy(200, [5, 6], 3, userId); 159 | }); 160 | 161 | after('Delete Weathy', async () => { 162 | await Weathy.destroy({ 163 | where: { 164 | id: weathyId 165 | }, 166 | paranoid: false 167 | }); 168 | }); 169 | 170 | it('modifyWeathy Success case test ', async function () { 171 | await weathyService.modifyWeathy( 172 | weathyId, 173 | userId, 174 | code, 175 | [6], 176 | 1, 177 | 'feedback was changed' 178 | ); 179 | 180 | const modifedWeathy = await Weathy.findOne({ 181 | include: [ 182 | { 183 | model: DailyWeather, 184 | required: true, 185 | attributes: ['location_id'] 186 | } 187 | ], 188 | where: { 189 | id: weathyId, 190 | user_id: userId 191 | } 192 | }); 193 | 194 | const result = await WeathyClothes.findAndCountAll({ 195 | where: { 196 | weathy_id: weathyId 197 | } 198 | }); 199 | 200 | assert.ok(modifedWeathy.description === 'feedback was changed'); 201 | assert.ok(modifedWeathy.emoji_id === 1); 202 | assert.ok(modifedWeathy.DailyWeather.location_id === code); 203 | assert.ok(result.count === 1); 204 | }); 205 | }); 206 | 207 | describe('modifyImgField Test', async function () { 208 | const userId = 1; 209 | let weathyId; 210 | before('Create Weathy', async function () { 211 | const weathy = await Weathy.create({ 212 | user_id: 1, 213 | dailyweather_id: 108, 214 | emoji_id: 3, 215 | description: 'hello', 216 | img_url: null 217 | }); 218 | weathyId = weathy.id; 219 | }); 220 | 221 | after('Delete Weathy', async function () { 222 | await Weathy.destroy({ 223 | where: { 224 | id: weathyId 225 | } 226 | }); 227 | }); 228 | 229 | it('Insert null into img_url', async function () { 230 | await weathyService.modifyImgField(null, weathyId, userId); 231 | const weathy = await Weathy.findOne({ 232 | where: { 233 | id: weathyId 234 | } 235 | }); 236 | 237 | assert.strictEqual(weathy.img_url, null); 238 | }); 239 | 240 | it('Insert hello world into img_url', async function () { 241 | const helloWorld = 'hello world!'; 242 | await weathyService.modifyImgField(helloWorld, weathyId, userId); 243 | const weathy = await Weathy.findOne({ 244 | where: { 245 | id: weathyId 246 | } 247 | }); 248 | assert.strictEqual(weathy.img_url, helloWorld); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /WeathyServer/tests/utils/dateUtils.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const dateUtils = require('../../utils/dateUtils'); 3 | 4 | describe('dateUtils test', function () { 5 | describe('getYear test', function () { 6 | it('Possible to parse year from YYYY-MM-DD', function () { 7 | assert.strictEqual(dateUtils.getYear('1999-00-00'), 1999); 8 | }); 9 | it('Possible to parse year from YYYY-MM', function () { 10 | assert.strictEqual(dateUtils.getYear('1999-00'), 1999); 11 | }); 12 | }); 13 | 14 | describe('getMonth test', function () { 15 | it('Possible to parse month from YYYY-MM-DD', function () { 16 | assert.strictEqual(dateUtils.getMonth('1999-04-00'), 4); 17 | }); 18 | it('Possible to parse month from YYYY-MM', function () { 19 | assert.strictEqual(dateUtils.getMonth('1999-04'), 4); 20 | }); 21 | }); 22 | 23 | describe('format12 test', function () { 24 | it('Format12 returns at 12pm', function () { 25 | assert.strictEqual(dateUtils.format12(12), '오후 12시'); 26 | }); 27 | it('Format12 returns at 12pm', function () { 28 | assert.strictEqual(dateUtils.format12(0), '오전 12시'); 29 | }); 30 | it('Format12 returns at 1pm', function () { 31 | assert.strictEqual(dateUtils.format12(13), '오후 1시'); 32 | }); 33 | it('Format12 returns at 1am', function () { 34 | assert.strictEqual(dateUtils.format12(1), '오전 1시'); 35 | }); 36 | }); 37 | 38 | describe('format24 test', function () { 39 | it('Format24 returns at 12pm', function () { 40 | assert.strictEqual(dateUtils.format24(12), '12시'); 41 | }); 42 | it('Format24 returns at 12pm', function () { 43 | assert.strictEqual(dateUtils.format24(0), '0시'); 44 | }); 45 | it('Format24 returns at 1pm', function () { 46 | assert.strictEqual(dateUtils.format24(13), '13시'); 47 | }); 48 | it('Format24 returns at 1am', function () { 49 | assert.strictEqual(dateUtils.format24(1), '1시'); 50 | }); 51 | }); 52 | 53 | describe('formatDate test', function () { 54 | it('formatDate returns YYYY-MM-DD formated string', function () { 55 | const date = new Date('1999-01-01'); 56 | assert.ok(dateUtils.formatDate(date) == '1999-01-01'); 57 | }); 58 | }); 59 | 60 | describe('getNextHour test', function () { 61 | it('getNextHour increase time', function () { 62 | const { next_date, next_time } = dateUtils.getNextHour( 63 | '1999-01-01', 64 | 12 65 | ); 66 | assert.strictEqual(next_date, '1999-01-01'); 67 | assert.strictEqual(next_time, 13); 68 | }); 69 | it('getNextHour increase date and reset time when date change', function () { 70 | const { next_date, next_time } = dateUtils.getNextHour( 71 | '1999-01-01', 72 | 23 73 | ); 74 | assert.strictEqual(next_date, '1999-01-02'); 75 | assert.strictEqual(next_time, 0); 76 | }); 77 | }); 78 | 79 | describe('getNextDay test', function () { 80 | it('getNextDay increase day', function () { 81 | const date = dateUtils.getNextDay('1999-01-01'); 82 | assert.strictEqual(date, '1999-01-02'); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /WeathyServer/tests/utils/tokenUtils.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const tokenUtils = require('../../utils/tokenUtils'); 3 | 4 | describe('tokenUtils test', function () { 5 | describe('generateToken test', function () { 6 | it('generateToken returns token contains userId', function () { 7 | const token = tokenUtils.generateToken(1); 8 | assert.ok(token.split(':')[0], 1); 9 | }); 10 | 11 | it('generateToken returns token contains 30 length random string', function () { 12 | const token = tokenUtils.generateToken(1); 13 | assert.ok(token.split(':')[1].length, 30); 14 | }); 15 | }); 16 | describe('getUserIdFromToken test', function () { 17 | it('getUserIdFromToken returns userId', function () { 18 | const token = '1:token'; 19 | assert.ok(tokenUtils.getUserIdFromToken(token), 1); 20 | }); 21 | }); 22 | describe('isUserOwnerOfToken test', function () { 23 | it('isUserOwnerOfToken returns true if userId matched', function () { 24 | const token = '1:token'; 25 | assert.ok(tokenUtils.isUserOwnerOfToken(1, token)); 26 | }); 27 | it('isUserOwnerOfToken returns false if userId unmatched', function () { 28 | const token = '1:token'; 29 | assert.ok(!tokenUtils.isUserOwnerOfToken(2, token)); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /WeathyServer/utils/dateUtils.js: -------------------------------------------------------------------------------- 1 | const day_of_week = [ 2 | '일요일', 3 | '월요일', 4 | '화요일', 5 | '수요일', 6 | '목요일', 7 | '금요일', 8 | '토요일' 9 | ]; 10 | 11 | const formatDate = (d) => { 12 | let month = '' + (d.getMonth() + 1); 13 | let day = '' + d.getDate(); 14 | const year = d.getFullYear(); 15 | if (month.length < 2) month = '0' + month; 16 | if (day.length < 2) day = '0' + day; 17 | return [year, month, day].join('-'); 18 | }; 19 | 20 | module.exports = { 21 | getYear: (date) => { 22 | return parseInt(date.split('-')[0]); 23 | }, 24 | getMonth: (date) => { 25 | return parseInt(date.split('-')[1]); 26 | }, 27 | getDay: (date) => { 28 | return parseInt(date.split('-')[2]); 29 | }, 30 | getYoil: (date) => { 31 | const today = new Date(date).getDay(); 32 | return day_of_week[today]; 33 | }, 34 | format24: (hour) => { 35 | return hour + '시'; 36 | }, 37 | format12: (hour) => { 38 | if (hour == 0) { 39 | return '오전 12시'; 40 | } else if (hour == 12) { 41 | return '오후 12시'; 42 | } else if (hour > 12) { 43 | return '오후 ' + (hour - 12) + '시'; 44 | } else { 45 | return '오전 ' + hour + '시'; 46 | } 47 | }, 48 | formatDate, 49 | getNextHour: (date, time) => { 50 | let day = new Date(date); 51 | ++time; 52 | if (time == 24) { 53 | day.setDate(day.getDate() + 1); 54 | time = 0; 55 | } 56 | return { next_date: formatDate(day), next_time: time }; 57 | }, 58 | getNextDay: (date) => { 59 | let day = new Date(date); 60 | day.setDate(day.getDate() + 1); 61 | return formatDate(day); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /WeathyServer/utils/tokenUtils.js: -------------------------------------------------------------------------------- 1 | const cryptoRandomString = require('crypto-random-string'); 2 | 3 | module.exports = { 4 | generateToken: (user_id) => { 5 | return ( 6 | user_id + 7 | ':' + 8 | cryptoRandomString({ length: 30, type: 'alphanumeric' }) 9 | ); 10 | }, 11 | getUserIdFromToken: (token) => { 12 | return token.split(':')[0]; 13 | }, 14 | isUserOwnerOfToken: (userId, token) => { 15 | return userId == token.split(':')[0]; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /WeathyServer/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /WeathyServer/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /WeathyServer/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | --------------------------------------------------------------------------------