diff --git a/.env.example b/.env.example index 769e0fa8..ea107da9 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,10 @@ CORS_WHITELIST=[CORS 정책에서 허용하는 도메인의 목록(e.g. ["http:/ GOOGLE_APPLICATION_CREDENTIALS=[GOOGLE_APPLICATION_CREDENTIALS JSON] TEST_ACCOUNTS=[스팍스SSO로 로그인시 무조건 테스트로 로그인이 가능한 허용 아이디 목록] SLACK_REPORT_WEBHOOK_URL=[Slack 웹훅 URL들이 담긴 JSON] + +# optional environment variables for taxiSampleGenerator +SAMPLE_NUM_OF_ROOMS=[방의 개수] +SAMPLE_NUM_OF_CHATS=[각 방의 채팅 개수] +SAMPLE_MAXIMUM_INTERVAL_BETWEEN_CHATS=[채팅 간 최대 시간 간격(단위: 초, 실수도 가능)] +SAMPLE_OCCURENCE_OF_JOIN=[새로운 채팅이 입장 메세지일 확률(0 ~ 1 사이의 값)] +SAMPLE_OCCURENCE_OF_ABORT=[새로운 채팅이 퇴장 메세지일 확률(0 ~ 1 사이의 값)] \ No newline at end of file diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index b08563d1..1f19bb94 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -31,20 +31,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - id: submodule-local - name: Save local version of submodule - run: echo "ver=`cd sampleGenerator && git log --pretty="%h" -1 && cd ..`" >> $GITHUB_OUTPUT - - id: submodule-origin - name: Save origin version of submodule - run: echo "ver=`cd sampleGenerator && git log origin --pretty="%h" -1 && cd ..`" >> $GITHUB_OUTPUT - - name: Check submodule version - if: ${{ steps.submodule-local.outputs.ver != steps.submodule-origin.outputs.ver }} - uses: actions/github-script@v3 - with: - script: | - core.setFailed('Please update submodule to the latest version by using \"git submodule update --remote\"') - - name: Install sampleGenerator dependencies from package-lock.json - run: cd sampleGenerator && pnpm i --force --frozen-lockfile && cd .. - name: Install taxi-back dependencies from package-lock.json run: pnpm i --force --frozen-lockfile - name: Run unit tests diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f15db4d8..00000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "sampleGenerator"] - path = sampleGenerator - url = https://github.com/sparcs-kaist/taxiSampleGenerator - branch = main diff --git a/README.md b/README.md index 26cda418..9a335131 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,3 @@ See [contributors](https://github.com/sparcs-kaist/taxi-front/graphs/contributor - app : https://github.com/sparcs-kaist/taxi-app - docker : https://github.com/sparcs-kaist/taxi-docker - figma : https://www.figma.com/file/li34hP1oStJAzLNjcG5KjN/SPARCS-Taxi?node-id=0%3A1 - - taxiSampleGenerator : https://github.com/sparcs-kaist/taxiSampleGenerator diff --git a/package.json b/package.json index d0a3d172..e9d3ff5e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "clean": "rimraf dist/", "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", "lint": "pnpm eslint .", - "sample": "cd sampleGenerator && npm start && cd .." + "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", + "sample": "cd src/sampleGenerator && npm start && cd .." }, "engines": { "node": ">=18.0.0", @@ -75,6 +76,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-mocha": "^10.1.0", "mocha": "^10.2.0", + "mongodb": "^4.1.0", "nodemon": "^3.0.1", "rimraf": "^5.0.5", "supertest": "^6.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72a2f835..469bd3d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ devDependencies: mocha: specifier: ^10.2.0 version: 10.2.0 + mongodb: + specifier: ^4.1.0 + version: 4.17.1 nodemon: specifier: ^3.0.1 version: 3.0.1 @@ -277,7 +280,6 @@ packages: '@aws-crypto/util': 3.0.0 '@aws-sdk/types': 3.425.0 tslib: 1.14.1 - dev: false optional: true /@aws-crypto/ie11-detection@3.0.0: @@ -285,7 +287,6 @@ packages: requiresBuild: true dependencies: tslib: 1.14.1 - dev: false optional: true /@aws-crypto/sha256-browser@3.0.0: @@ -300,7 +301,6 @@ packages: '@aws-sdk/util-locate-window': 3.310.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 - dev: false optional: true /@aws-crypto/sha256-js@3.0.0: @@ -310,7 +310,6 @@ packages: '@aws-crypto/util': 3.0.0 '@aws-sdk/types': 3.425.0 tslib: 1.14.1 - dev: false optional: true /@aws-crypto/supports-web-crypto@3.0.0: @@ -318,7 +317,6 @@ packages: requiresBuild: true dependencies: tslib: 1.14.1 - dev: false optional: true /@aws-crypto/util@3.0.0: @@ -328,7 +326,6 @@ packages: '@aws-sdk/types': 3.425.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 - dev: false optional: true /@aws-sdk/client-cognito-identity@3.427.0: @@ -375,7 +372,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/client-sso@3.427.0: @@ -419,7 +415,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/client-sts@3.427.0: @@ -467,7 +462,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/credential-provider-cognito-identity@3.427.0: @@ -482,7 +476,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/credential-provider-env@3.425.0: @@ -494,7 +487,6 @@ packages: '@smithy/property-provider': 2.0.12 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/credential-provider-http@3.425.0: @@ -509,7 +501,6 @@ packages: '@smithy/protocol-http': 3.0.7 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/credential-provider-ini@3.427.0: @@ -529,7 +520,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/credential-provider-node@3.427.0: @@ -550,7 +540,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/credential-provider-process@3.425.0: @@ -563,7 +552,6 @@ packages: '@smithy/shared-ini-file-loader': 2.2.0 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/credential-provider-sso@3.427.0: @@ -580,7 +568,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/credential-provider-web-identity@3.425.0: @@ -592,7 +579,6 @@ packages: '@smithy/property-provider': 2.0.12 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/credential-providers@3.427.0: @@ -618,7 +604,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/middleware-host-header@3.425.0: @@ -630,7 +615,6 @@ packages: '@smithy/protocol-http': 3.0.7 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/middleware-logger@3.425.0: @@ -641,7 +625,6 @@ packages: '@aws-sdk/types': 3.425.0 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/middleware-recursion-detection@3.425.0: @@ -653,7 +636,6 @@ packages: '@smithy/protocol-http': 3.0.7 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/middleware-sdk-sts@3.425.0: @@ -665,7 +647,6 @@ packages: '@aws-sdk/types': 3.425.0 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/middleware-signing@3.425.0: @@ -680,7 +661,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/util-middleware': 2.0.4 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/middleware-user-agent@3.427.0: @@ -693,7 +673,6 @@ packages: '@smithy/protocol-http': 3.0.7 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/region-config-resolver@3.425.0: @@ -706,7 +685,6 @@ packages: '@smithy/util-config-provider': 2.0.0 '@smithy/util-middleware': 2.0.4 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/token-providers@3.427.0: @@ -751,7 +729,6 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - aws-crt - dev: false optional: true /@aws-sdk/types@3.425.0: @@ -761,7 +738,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/util-endpoints@3.427.0: @@ -772,7 +748,6 @@ packages: '@aws-sdk/types': 3.425.0 '@smithy/node-config-provider': 2.1.1 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/util-locate-window@3.310.0: @@ -781,7 +756,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@aws-sdk/util-user-agent-browser@3.425.0: @@ -792,7 +766,6 @@ packages: '@smithy/types': 2.3.5 bowser: 2.11.0 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/util-user-agent-node@3.425.0: @@ -809,7 +782,6 @@ packages: '@smithy/node-config-provider': 2.1.1 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@aws-sdk/util-utf8-browser@3.259.0: @@ -817,7 +789,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@babel/code-frame@7.22.5: @@ -2334,7 +2305,7 @@ packages: '@firebase/database-types': 0.10.4 '@firebase/logger': 0.4.0 '@firebase/util': 1.9.3 - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@firebase/database-types@0.10.4: @@ -2595,7 +2566,6 @@ packages: requiresBuild: true dependencies: sparse-bitfield: 3.0.3 - dev: false optional: true /@nodelib/fs.scandir@2.1.5: @@ -2861,7 +2831,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/config-resolver@2.0.14: @@ -2874,7 +2843,6 @@ packages: '@smithy/util-config-provider': 2.0.0 '@smithy/util-middleware': 2.0.4 tslib: 2.6.2 - dev: false optional: true /@smithy/credential-provider-imds@2.0.16: @@ -2887,7 +2855,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/url-parser': 2.0.11 tslib: 2.6.2 - dev: false optional: true /@smithy/eventstream-codec@2.0.11: @@ -2898,7 +2865,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/util-hex-encoding': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/fetch-http-handler@2.2.2: @@ -2910,7 +2876,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/util-base64': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/hash-node@2.0.11: @@ -2922,7 +2887,6 @@ packages: '@smithy/util-buffer-from': 2.0.0 '@smithy/util-utf8': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/invalid-dependency@2.0.11: @@ -2931,7 +2895,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/is-array-buffer@2.0.0: @@ -2940,7 +2903,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/middleware-content-length@2.0.13: @@ -2951,7 +2913,6 @@ packages: '@smithy/protocol-http': 3.0.7 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/middleware-endpoint@2.0.11: @@ -2964,7 +2925,6 @@ packages: '@smithy/url-parser': 2.0.11 '@smithy/util-middleware': 2.0.4 tslib: 2.6.2 - dev: false optional: true /@smithy/middleware-retry@2.0.16: @@ -2980,7 +2940,6 @@ packages: '@smithy/util-retry': 2.0.4 tslib: 2.6.2 uuid: 8.3.2 - dev: false optional: true /@smithy/middleware-serde@2.0.11: @@ -2990,7 +2949,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/middleware-stack@2.0.5: @@ -3000,7 +2958,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/node-config-provider@2.1.1: @@ -3012,7 +2969,6 @@ packages: '@smithy/shared-ini-file-loader': 2.2.0 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/node-http-handler@2.1.7: @@ -3025,7 +2981,6 @@ packages: '@smithy/querystring-builder': 2.0.11 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/property-provider@2.0.12: @@ -3035,7 +2990,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/protocol-http@3.0.7: @@ -3045,7 +2999,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/querystring-builder@2.0.11: @@ -3056,7 +3009,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/util-uri-escape': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/querystring-parser@2.0.11: @@ -3066,7 +3018,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/service-error-classification@2.0.4: @@ -3075,7 +3026,6 @@ packages: requiresBuild: true dependencies: '@smithy/types': 2.3.5 - dev: false optional: true /@smithy/shared-ini-file-loader@2.2.0: @@ -3085,7 +3035,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/signature-v4@2.0.11: @@ -3101,7 +3050,6 @@ packages: '@smithy/util-uri-escape': 2.0.0 '@smithy/util-utf8': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/smithy-client@2.1.10: @@ -3113,7 +3061,6 @@ packages: '@smithy/types': 2.3.5 '@smithy/util-stream': 2.0.15 tslib: 2.6.2 - dev: false optional: true /@smithy/types@2.3.5: @@ -3122,7 +3069,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/url-parser@2.0.11: @@ -3132,7 +3078,6 @@ packages: '@smithy/querystring-parser': 2.0.11 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/util-base64@2.0.0: @@ -3142,7 +3087,6 @@ packages: dependencies: '@smithy/util-buffer-from': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/util-body-length-browser@2.0.0: @@ -3150,7 +3094,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/util-body-length-node@2.1.0: @@ -3159,7 +3102,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/util-buffer-from@2.0.0: @@ -3169,7 +3111,6 @@ packages: dependencies: '@smithy/is-array-buffer': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/util-config-provider@2.0.0: @@ -3178,7 +3119,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/util-defaults-mode-browser@2.0.14: @@ -3191,7 +3131,6 @@ packages: '@smithy/types': 2.3.5 bowser: 2.11.0 tslib: 2.6.2 - dev: false optional: true /@smithy/util-defaults-mode-node@2.0.18: @@ -3206,7 +3145,6 @@ packages: '@smithy/smithy-client': 2.1.10 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/util-hex-encoding@2.0.0: @@ -3215,7 +3153,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/util-middleware@2.0.4: @@ -3225,7 +3162,6 @@ packages: dependencies: '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/util-retry@2.0.4: @@ -3236,7 +3172,6 @@ packages: '@smithy/service-error-classification': 2.0.4 '@smithy/types': 2.3.5 tslib: 2.6.2 - dev: false optional: true /@smithy/util-stream@2.0.15: @@ -3252,7 +3187,6 @@ packages: '@smithy/util-hex-encoding': 2.0.0 '@smithy/util-utf8': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@smithy/util-uri-escape@2.0.0: @@ -3261,7 +3195,6 @@ packages: requiresBuild: true dependencies: tslib: 2.6.2 - dev: false optional: true /@smithy/util-utf8@2.0.0: @@ -3271,7 +3204,6 @@ packages: dependencies: '@smithy/util-buffer-from': 2.0.0 tslib: 2.6.2 - dev: false optional: true /@socket.io/component-emitter@3.1.0: @@ -3995,14 +3927,12 @@ packages: /@types/webidl-conversions@7.0.0: resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==} - dev: false /@types/whatwg-url@8.2.2: resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} dependencies: '@types/node': 20.9.0 '@types/webidl-conversions': 7.0.0 - dev: false /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2): resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} @@ -4516,7 +4446,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false /base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} @@ -4575,7 +4504,6 @@ packages: /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} requiresBuild: true - dev: false optional: true /brace-expansion@1.1.11: @@ -4616,7 +4544,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: buffer: 5.7.1 - dev: false /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -4639,7 +4566,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.1.13 - dev: false /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} @@ -5840,7 +5766,6 @@ packages: requiresBuild: true dependencies: strnum: 1.0.5 - dev: false optional: true /fast-xml-parser@4.2.7: @@ -6439,7 +6364,6 @@ packages: /ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} - dev: false /ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -6482,7 +6406,6 @@ packages: /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} - dev: false /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} @@ -7198,7 +7121,6 @@ packages: /memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} requiresBuild: true - dev: false optional: true /merge-descriptors@1.0.1: @@ -7347,7 +7269,6 @@ packages: dependencies: '@types/whatwg-url': 8.2.2 whatwg-url: 11.0.0 - dev: false /mongodb@4.17.1: resolution: {integrity: sha512-MBuyYiPUPRTqfH2dV0ya4dcr2E5N52ocBuZ8Sgg/M030nGF78v855B3Z27mZJnp8PxjnUquEnAtjOsphgMZOlQ==} @@ -7361,7 +7282,6 @@ packages: '@mongodb-js/saslprep': 1.1.0 transitivePeerDependencies: - aws-crt - dev: false /mongoose@6.12.0: resolution: {integrity: sha512-sd/q83C6TBRPBrrD2A/POSbA/exbCFM2WOuY7Lf2JuIJFlHFG39zYSDTTAEiYlzIfahNOLmXPxBGFxdAch41Mw==} @@ -8723,7 +8643,6 @@ packages: /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: false /socket.io-adapter@2.5.2: resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} @@ -8767,7 +8686,6 @@ packages: dependencies: ip: 2.0.0 smart-buffer: 4.2.0 - dev: false /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -8796,7 +8714,6 @@ packages: requiresBuild: true dependencies: memory-pager: 1.5.0 - dev: false optional: true /stack-trace@0.0.10: @@ -8895,7 +8812,6 @@ packages: /strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} requiresBuild: true - dev: false optional: true /stubs@3.0.0: @@ -9121,7 +9037,6 @@ packages: engines: {node: '>=12'} dependencies: punycode: 2.3.0 - dev: false /triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} @@ -9161,16 +9076,10 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} requiresBuild: true - dev: false optional: true - /tslib@2.6.1: - resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} - dev: false - /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} @@ -9403,7 +9312,6 @@ packages: /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - dev: false /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} @@ -9454,7 +9362,6 @@ packages: /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - dev: false /websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} @@ -9476,7 +9383,6 @@ packages: dependencies: tr46: 3.0.0 webidl-conversions: 7.0.0 - dev: false /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} diff --git a/sampleGenerator b/sampleGenerator deleted file mode 160000 index bd4329c1..00000000 --- a/sampleGenerator +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bd4329c15405a09c94e7b78e19ff296b4c2d0fb3 diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js new file mode 100644 index 00000000..78ebe778 --- /dev/null +++ b/scripts/profileImageUrlUpdater.js @@ -0,0 +1,36 @@ +// Issue #173을 해결하기 위한 DB 마이그레이션 스크립트입니다. +// https://github.com/sparcs-kaist/taxi-back/issues/173 + +const { MongoClient } = require("mongodb"); +const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); + +const time = Date.now(); + +const client = new MongoClient(mongoUrl); +const db = client.db("taxi"); +const users = db.collection("users"); + +async function run() { + try { + for await (const doc of users.find()) { + // 이미 변환이 완료된 경우에는 Pass합니다. + if (doc.profileImageUrl.startsWith(awsEnv.s3Url)) continue; + + await users.findOneAndUpdate( + { _id: doc._id }, + { + $set: { + profileImageUrl: `${awsEnv.s3Url}/profile-img/${doc.profileImageUrl}?token=${time}`, + }, + } + ); + } + } catch (err) { + console.log(err); + } finally { + await client.close(); + } +} +run().then(() => { + console.log("Done!"); +}); diff --git a/src/index.ts b/src/index.ts index ef692ea5..1b3d00f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import http from "http"; -import { nodeEnv, port as httpPort } from "@/loadenv"; +import { nodeEnv, mongo as mongoUrl, port as httpPort } from "@/loadenv"; import { corsMiddleware, sessionMiddleware, @@ -38,7 +38,7 @@ initializeApp(); const app = express(); // 데이터베이스 연결 -connectDatabase(); +connectDatabase(mongoUrl); // [Middleware] request body 파싱 app.use(express.urlencoded({ extended: false })); diff --git a/src/modules/modifyProfile.js b/src/modules/modifyProfile.js index 0d15bd58..e8702f98 100755 --- a/src/modules/modifyProfile.js +++ b/src/modules/modifyProfile.js @@ -1,4 +1,5 @@ const crypto = require("crypto"); +const aws = require("./stores/aws"); const nouns = [ "재료역학", @@ -81,7 +82,7 @@ const generateNickname = (id) => { // 기존 프로필 사진의 URI 중 하나를 무작위로 선택해 반환합니다. const generateProfileImageUrl = () => { const ridx = crypto.randomInt(defaultProfile.length); - return `default/${defaultProfile[ridx]}`; + return aws.getS3Url(`/profile-img/default/${defaultProfile[ridx]}`); }; // 사용자의 이름과 성을 받아, 한글인지 영어인지에 따라 전체 이름을 반환합니다. diff --git a/src/modules/socket.js b/src/modules/socket.js index eabbbe62..cd1d7d95 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -219,9 +219,11 @@ const emitUpdateEvent = async (io, roomId) => { throw new IllegalArgumentsException(); } - part.forEach(({ user }) => io.in(`user-${user}`).emit("chat_update"), { - roomId, - }); + part.forEach(({ user }) => + io.in(`user-${user}`).emit("chat_update", { + roomId, + }) + ); return true; } catch (err) { diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index f4741652..7aaebb27 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -1,7 +1,6 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; -const { mongo: mongoUrl } = require("@/loadenv"); const logger = require("@/modules/logger"); const userSchema = Schema({ @@ -184,23 +183,27 @@ database.on("error", function (err) { logger.error("데이터베이스 연결 에러 발생: " + err); mongoose.disconnect(); }); -database.on("disconnected", function () { - // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. - logger.error("데이터베이스와 연결이 끊어졌습니다!"); - setTimeout(() => { - mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - }, 5000); -}); -const connectDatabase = () => +const connectDatabase = (mongoUrl) => { + database.on("disconnected", function () { + // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. + logger.error("데이터베이스와 연결이 끊어졌습니다!"); + setTimeout(() => { + mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + }, 5000); + }); + mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, }); + return database; +}; + module.exports = { connectDatabase, userModel: mongoose.model("User", userSchema), diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js new file mode 100644 index 00000000..97fc1352 --- /dev/null +++ b/src/routes/docs/chats.js @@ -0,0 +1,513 @@ +const { objectIdPattern } = require("./utils"); + +const tag = "chats"; +const apiPrefix = "/chats"; + +const chatsDocs = {}; +chatsDocs[`${apiPrefix}`] = { + post: { + tags: [tag], + summary: "가장 최근 채팅 가져오기", + description: "가장 최근에 도착한 60개의 채팅을 가져옵니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅을 보내는 방의 id", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + examples: { + "소켓 연결 오류": { value: "Chat/ : socket did not connected" }, + "유저가 방에 참여하지 않음": { + value: "Chat/ : user did not participated in the room", + }, + }, + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/ : internal server error", + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/load/before`] = { + post: { + tags: [tag], + summary: "특정 시점 이전의 채팅 가져오기", + description: "lastMsgDate 이전에 도착한 60개의 채팅을 가져옵니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅을 보내는 방의 id", + }, + lastMsgDate: { + type: "string", + format: "date-time", + description: "이전 채팅을 가져올 특정 시점", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + examples: { + "소켓 연결 오류": { + value: "Chat/load/before : socket did not connected", + }, + "유저가 방에 참여하지 않음": { + value: + "Chat/load/before : user did not participated in the room", + }, + }, + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/load/before : internal server error", + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/load/after`] = { + post: { + tags: [tag], + summary: "특정 시점 이후 채팅 가져오기", + description: "lastMsgDate 이후에 도착한 60개의 채팅을 가져옵니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅을 보내는 방의 id", + }, + lastMsgDate: { + type: "string", + format: "date-time", + description: "이전 채팅을 가져올 특정 시점", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + examples: { + "소켓 연결 오류": { + value: "Chat/load/after : socket did not connected", + }, + "유저가 방에 참여하지 않음": { + value: + "Chat/load/after : user did not participated in the room", + }, + }, + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/load/after : internal server error", + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/send`] = { + post: { + tags: [tag], + summary: "채팅 요청 처리", + description: `채팅 요청을 처리합니다.
+ socker 통신을 통하여 같은 방에 있는 user들에게 이 채팅을 전송합니다.
+
+ 채팅 기록은 아래와 같이 구성됩니다.
+ + Chat { + roomId: ObjectId, //방의 objectId + type: String, // 메시지 종류 ("text": 일반 메시지, "s3img": S3에 업로드된 이미지, "in": 입장 메시지, "out": 퇴장 메시지, "payment": 결제 메시지, "settlement": 정산 완료 메시지, "account": 계좌 전송 메시지) + authorId: ObejctId, //작성자의 objectId + content: String, // 메시지 내용 (메시지 종류에 따라 포맷이 상이함) + time: String(ISO 8601), // ex) 2024-01-08T01:52:00.000Z + isValid: Boolean, // 클라이언트가 보낸 메시지가 유효한 지 여부. 클라이언트가 이미지를 업로드했을 때, 해당 이미지가 제대로 업로드됐는지 확인하기 전까지 이미지를 보여주지 않기 위해 사용됨. + } + `, + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅을 보내는 방의 id", + }, + type: { + type: "string", + enum: [ + "text", + "s3img", + "in", + "out", + "payment", + "settlement", + "account", + "departure", + "arrival", + ], + description: `채팅 메시지의 유형
+ 일반 text의 경우 *text*, 사진의 경우 *s3img*
+ 기타 종류의 채팅의 경우(입장, 퇴장 메시지 등) 정해진 type의 채팅을 사용`, + }, + content: { + type: "string", + example: "안녕하세요~!", + description: "채팅 메세지의 본문", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + examples: { + "소켓 연결 오류": { + value: "Chat/send : socket did not connected", + }, + "유저가 방에 참여하지 않음": { + value: "Chat/send : user did not participated in the room", + }, + }, + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/send : internal server error", + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/read`] = { + post: { + tags: [tag], + summary: "채팅 읽은 시각 업데이트 요청", + description: `채팅 읽은 시각의 업데이트 요청을 처리합니다.
+ socket 통신을 통하여 같은 방에 있는 user들에게 업데이트를 요청합니다.`, + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅을 보내는 방의 id", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + example: "Chat/read : socket did not connected", + }, + }, + }, + 404: { + content: { + "text/html": { + example: "Chat/read : cannot find room info", + }, + }, + }, + 500: { + content: { + "text/html": { + examples: { + "소켓 이벤트 전송 오류": { + value: "Chat/read : failed to emit socket events", + }, + "기타 서버 오류": { + value: "Chat/read : internal server error", + }, + }, + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/uploadChatImg/getPUrl`] = { + post: { + tags: [tag], + summary: "채팅 이미지를 업로드할 수 있는 Presigned-url을 발급", + description: `채팅 이미지를 업로드 하기 위한 Presigned-url을 발급합니다.
+ 이미지 전송을 위해 \`s3img\` 형식의 chat document를 생성 후 저장하며,
+ presigned-url은 aws S3 api를 통해 생성됩니다.`, + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅 이미지를 보내는 방의 id", + }, + type: { + type: "string", + enum: ["image/png", "image/jpg", "image/jpeg"], + description: "채팅 이미지의 파일 형식", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { + type: "string", + pattern: objectIdPattern, + description: "생성된 chat Document의 object id", + }, + url: { + type: "string", + example: "https://s3.{region}.amazonaws.com/{bucket-name}", + description: "taxi s3 url 주소", + }, + fields: { + type: "object", + properties: { + bucket: { + type: "string", + example: "bucket-name", + }, + "Content-Type": { + type: "string", + enum: ["image/png", "image/jpg", "image/jpeg"], + }, + key: { + type: "string", + pattern: `^chat-img/[a-fA-F\d]{24}$`, + }, + }, + description: "image의 key, type, bucket와 같은 정보", + }, + }, + }, + }, + }, + }, + 403: { + content: { + "text/html": { + example: "Chat/uploadChatImg/getPUrl : did not joined the chatting", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/uploadChatImg/getPUrl : internal server error", + }, + }, + }, + }, + }, +}; + +chatsDocs[`${apiPrefix}/uploadChatImg/done`] = { + post: { + tags: [tag], + summary: "채팅 이미지 업로드 완료 여부 확인", + description: `채팅 이미지가 제대로 업로드 되었는지 확인합니다.
+ 이미지가 제대로 업로드 되었다면, socket 통신을 통해 채팅 이미지를 전송합니다.
+ 이때 채팅의 \`content\`에는 s3에 저장된 url을 나타내는 채팅의 object id를 넣어줍니다.`, + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + roomId: { + type: "string", + pattern: objectIdPattern, + description: "채팅 이미지를 보내는 방의 id", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + value: true, + }, + }, + }, + }, + }, + }, + 404: { + content: { + "text/html": { + example: "Chat/uploadChatImg/done : no corresponding chat", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Chat/uploadChatImg/getPUrl : internal server error", + }, + }, + }, + }, + }, +}; + +module.exports = chatsDocs; diff --git a/src/routes/docs/chats.md b/src/routes/docs/chats.md deleted file mode 100644 index 307d2def..00000000 --- a/src/routes/docs/chats.md +++ /dev/null @@ -1,99 +0,0 @@ -## `chats`: 채팅 시 발생하는 이벤트 정리 - -Taxi의 채팅 기능은 Socket.IO 라이브러리를 이용해 구현되어 있습니다. -클라이언트에서의 일반적인 Socket.IO 사용법은 [공식 문서](https://socket.io/docs/v4/client-socket-instance/)를 참조해주세요. -아래와 같은 채팅 이벤트들이 구현되어 있습니다. - -- `chats-join` -- `chats-receive` -- `chats-send` -- `chats-load` -- `chats-disconnected` (삭제해야 함) - -서버로부터 받은 모든 채팅 기록은 아래와 같은 자료형으로 구성되어 있습니다. - -```javascript -Chat { - roomId: ObjectId, //방의 objectId - type: String, // 메시지 종류("text": 일반 메시지, "in": 입장 메시지, "out": 퇴장 메시지, "s3img": S3에 업로드된 이미지, "payment": 결제 메시지, "settlement": 정산 완료 메시지) - authorId: ObejctId, //작성자의 objectId - content: String, // 메시지 내용(메시지 종류에 따라 포맷이 상이하며, 하단 참조) - time: String(ISO 8601), // ex) '2022-01-12T13:58:20.180Z' - isValid: Boolean, // 클라이언트가 보낸 메시지가 유효한 지 여부. 클라이언트가 이미지를 업로드했을 때, 해당 이미지가 제대로 업로드됐는지 확인하기 전까지 이미지를 보여주지 않기 위해 사용됨. -} -``` - -### 1. `chats-join` - -방에 새 사용자가 참여할 때 이 이벤트를 발생시키세요. -필요한 인자는 `roomId`입니다. - -- `roomId`: 방의 ObjectID (`String`), Socket.IO 서버와 연결을 시도할 때 사용자는 로그인이 되어 있어야 하며, 들어가려는 채팅방에 참여자로 참여하고 있어야 합니다. - -```javascript -const socket = io(server_address, { withCredentials: true }); -socket.emit("chats-join", roomId); -``` - -채팅방 접속이 정상적으로 완료되면, Socket.io 서버는 최근 30개의 메시지들(`Chat` 배열)을 전송합니다. - -```javascript -socket.on("chats-join", (chats) => { - // 최근 30개의 채팅 메시지 출력 - console.log(chats); -}); -``` - -### 2. `chats-send` - -채팅 메시지를 보낼 때 이 이벤트를 발생시키세요. -필요한 인자는 `roomId`와 `content`입니다. - -- `roomId`: 참여중인 방의 ObjectID(`String`) -- `content`: 보낼 텍스트(`String`) - -```javascript -socket.emit("chats-send", { roomId, content }); -``` - -메시지 전송이 성공/실패하면, Socket.IO 서버도 `chats-send` 이벤트를 발생시킵니다. - -```javascript -socket.on("chats-send", (response) => { - // 최근 30개의 채팅 메시지 출력 - console.log(response); -}); -``` - -`response`는 전송이 성공했을 경우 `{done: true}`, 실패했을 경우 `{err: true}`입니다. - -### 3. `chats-receive` - -이 이벤트는 서버나 다른 사용자가 채팅 메시지를 전송했을 때 발생합니다. 아래와 같이 `chat`에 접근하여 해당 메시지의 내용을 확인할 수 있습니다. - -```javascript -socket.on("chats-receive", (chat) => { - // 새로운 메시지 출력 - console.log(chat); -}); -``` - -### 4. `chats-load` - -과거 대화 목록을 더 불러오려면 이 이벤트를 발생시키세요. 필요한 인자는 `lastDate`와 `amount`(선택 사항) 입니다. - -- `lastDate`: 현재 클라이언트에서 불러온 채팅들 중 가장 오래된 것의 생성 시각. 서버는 이보다 먼저 생성된 메시지들을 반환합니다. ISO8601을 만족하는 `String`이어야 합니다. e.g.) `"2022-03-15T13:57:04.732Z"` -- `amount` (선택 사항): 불러올 과거 메시지의 수. 1~50의 자연수여야 하며, 입력하지 않은 경우 30개의 메시지를 가져옵니다. - -```javascript -socket.emit("chats-load", { lastDate: "2022-03-15T13:57:04.732Z", amount: 30 }); -``` - -`chats-load` 이벤트가 발생하면 서버는 클라이언트에 다시 `chats-load` 이벤트를 발생시켜 과거 채팅들(`Chat` 배열)을 보냅니다. - -```javascript -socket.on("chats-load", (chats) => { - // 과거 메시지들 출력 - console.log(chats); -}); -``` diff --git a/src/routes/docs/locations.js b/src/routes/docs/locations.js index 1fb445b9..34773482 100644 --- a/src/routes/docs/locations.js +++ b/src/routes/docs/locations.js @@ -6,8 +6,8 @@ locationsDocs[`${apiPrefix}`] = { get: { tags: [tag], summary: "출발지/도착지 정보 반환", - description: - "출발지/도착지로 사용 가능한 장소 목록 조회 및 요청 처리 당시 서버 시각 반환
\n (로그인된 상태에서만 접근 가능)", + description: `출발지/도착지로 사용 가능한 장소 목록 조회 및 요청 처리 당시 서버 시각 반환
+ (로그인된 상태에서만 접근 가능)`, responses: { 200: { description: "서버에 저장된 location이 없을 경우, locations은 빈 배열", diff --git a/src/routes/docs/logininfo.js b/src/routes/docs/logininfo.js index e752d3b0..59b5e0a8 100644 --- a/src/routes/docs/logininfo.js +++ b/src/routes/docs/logininfo.js @@ -1,3 +1,5 @@ +const { objectIdPattern } = require("./utils"); + const tag = "logininfo"; const apiPrefix = "/logininfo"; @@ -18,6 +20,7 @@ logininfoDocs[`${apiPrefix}`] = { properties: { oid: { type: "string", + type: objectIdPattern, }, id: { type: "string", diff --git a/src/routes/docs/reports.js b/src/routes/docs/reports.js index 0f5b8e76..b3976e7b 100644 --- a/src/routes/docs/reports.js +++ b/src/routes/docs/reports.js @@ -1,3 +1,5 @@ +const { objectIdPattern } = require("./utils"); + const tag = "reports"; const apiPrefix = "/reports"; @@ -57,9 +59,17 @@ reportsDocs[`${apiPrefix}/searchByUser`] = { properties: { reporting: { type: "array", + items: { + type: "string", + pattern: objectIdPattern, + }, }, reported: { type: "array", + items: { + type: "string", + pattern: objectIdPattern, + }, }, }, }, diff --git a/src/routes/docs/reportsSchema.js b/src/routes/docs/reportsSchema.js index 841918cf..654e1178 100644 --- a/src/routes/docs/reportsSchema.js +++ b/src/routes/docs/reportsSchema.js @@ -1,3 +1,5 @@ +const { objectIdPattern } = require("./utils"); + const reportsSchema = { createHandler: { type: "object", @@ -5,7 +7,7 @@ const reportsSchema = { properties: { reportedId: { type: "string", - pattern: "^[a-fA-F\\d]{24}$", + pattern: objectIdPattern, }, type: { type: "string", diff --git a/src/routes/docs/swaggerDocs.js b/src/routes/docs/swaggerDocs.js index 84f4e040..c0b474b4 100644 --- a/src/routes/docs/swaggerDocs.js +++ b/src/routes/docs/swaggerDocs.js @@ -4,6 +4,7 @@ const logininfoDocs = require("./logininfo"); const locationsDocs = require("./locations"); const authDocs = require("./auth"); const usersDocs = require("./users"); +const chatsDocs = require("./chats"); const { port, nodeEnv } = require("../../../loadenv"); const serverList = [ @@ -56,6 +57,10 @@ const swaggerDocs = { name: "users", description: "유저 계정 정보 수정 및 조회", }, + { + name: "chats", + description: "채팅 시 발생하는 이벤트 정리", + }, ], consumes: ["application/json"], produces: ["application/json"], @@ -65,6 +70,7 @@ const swaggerDocs = { ...locationsDocs, ...usersDocs, ...authDocs, + ...chatsDocs, }, components: { schemas: { diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index e76cd2b4..3cd8aa96 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -2,6 +2,120 @@ const tag = "users"; const apiPrefix = "/users"; const usersDocs = {}; +usersDocs[`${apiPrefix}/agreeOnTermsOfService`] = { + post: { + tags: [tag], + summary: "이용 약관에 동의", + description: + "요청을 보낸 유저의 약관 동의 여부를 동의함으로 변경합니다. 철회는 불가능합니다.", + responses: { + 200: { + content: { + "text/html": { + example: + "Users/agreeOnTermsOfService : agree on Terms of Service successful", + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/agreeOnTermsOfService : already agreed", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/agreeOnTermsOfService : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/getAgreeOnTermsOfService`] = { + get: { + tags: [tag], + summary: "이용 약관 동의 여부 전송", + description: + "요청을 보낸 유저의 이용 약관 동의 여부를 json 형태로 전송합니다.", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + agreeOnTermsOfService: { + type: "boolean", + description: "유저의 이용 약관 동의 여부", + example: true, + }, + }, + }, + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/getAgreeOnTermsOfService : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/editNickname`] = { + post: { + tags: [tag], + summary: "유저의 닉네임 변경", + description: "유저의 닉네임을 요청한 닉네임으로 변경합니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + nickname: { + type: "string", + example: "끈질긴 열과 분자의 이동", + description: "유저의 새 닉네임", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "text/html": { + example: "Users/editNickname : edit user nickname successful", + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/editNickname : such user id does not exist", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/editNickname : internal server error", + }, + }, + }, + }, + }, +}; + usersDocs[`${apiPrefix}/resetNickname`] = { get: { tags: [tag], @@ -11,21 +125,171 @@ usersDocs[`${apiPrefix}/resetNickname`] = { 200: { content: { "text/html": { - example: "User/resetNickname : reset user nickname successful", + example: "Users/resetNickname : reset user nickname successful", + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/resetNickname : such user does not exist", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/resetNickname : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/editAccount`] = { + post: { + tags: [tag], + summary: "유저의 계좌 번호 변경", + description: "유저의 계좌 번호를 요청한 계좌 번호로 변경합니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + account: { + type: "string", + description: "유저의 새 계좌 번호", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "text/html": { + example: "Users/editAccount : edit user account successful", }, }, }, 400: { content: { "text/html": { - example: "User/resetNickname : such user does not exist", + example: "Users/editAccount : such user id does not exist", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/editAccount : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/editProfileImg/getPUrl`] = { + post: { + tags: [tag], + summary: "프로필 이미지 업로드를 위한 presigned-url 발급", + description: `유저의 프로필 이미지는 AWS S3에서 관리됩니다. 변경할 프로필을 업로드 하기 위한 주소인 presigned-url을 발급합니다.
+
+ **프로필 사진은 아래 규칙을 만족해야 합니다:**
+ 1. 파일 형식은 image/png, image/jpg, image/jpeg 중 하나
+ 2. 파일 크기는 최대 50 MB`, + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + type: { + type: "string", + description: "업로드할 이미지 type", + example: "image/png", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + url: { + type: "string", + description: "이미지 업로드를 위한 presigned-url", + }, + fields: { + type: "object", + properties: { + "Content-Type": { + type: "string", + description: "이미지의 type", + }, + key: { + type: "string", + description: "이미지의 S3 파일 경로", + }, + }, + description: "업로드 파일의 type 및 key 정보", + }, + }, + }, }, }, }, 500: { + contnet: { + "text/html": { + example: "Users/editProfileImg/getPUrl : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/editProfileImg/done`] = { + get: { + tags: [tag], + summary: "프로필 이미지 정상 업로드 여부 확인", + description: `프로필 이미지가 S3에 정상적으로 업로드 되었는지 확인합니다.
+ 정상적으로 확인 되었다면, 유저의 \`profileImageUrl\` 정보를 새 프로필 이미지 파일명으로 업데이트 합니다.`, + responses: { + 200: { content: { + "application/json": { + schema: { + type: "object", + properties: { + result: { + type: "boolean", + description: "정상적인 업로드 성공 여부", + }, + profileImageUrl: { + type: "string", + description: + "새 프로필 이미지 파일명 (업로드 실패 시 `undefined`)", + }, + }, + }, + }, + }, + }, + 500: { + contnet: { "text/html": { - example: "User/resetNickname : internal server error", + example: "Users/editProfileImg/done : internal server error", }, }, }, @@ -43,21 +307,21 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { content: { "text/html": { example: - "User/resetProfileImg : reset user profile image successful", + "Users/resetProfileImg : reset user profile image successful", }, }, }, 400: { content: { "text/html": { - example: "User/resetProfileImg : such user does not exist", + example: "Users/resetProfileImg : such user does not exist", }, }, }, 500: { content: { "text/html": { - example: "User/resetProfileImg : internal server error", + example: "Users/resetProfileImg : internal server error", }, }, }, diff --git a/src/routes/docs/users.md b/src/routes/docs/users.md deleted file mode 100755 index 6b46963b..00000000 --- a/src/routes/docs/users.md +++ /dev/null @@ -1,305 +0,0 @@ -## `/users` - -- 사용자 정보 조회 및 수정 기능을 지원하는 API. -- 로그인된 상태에서만 접근 가능 -- 사용자를 반환할 경우 그 type은 다음과 같다. - -```javascript -User { - name: String, - nickname: String, // 3글자 이상 25글자 이하로 구성되며 영어 대소문자, 한글, " ", 0~9, "-", "_" 으로만 이루어져야 함. - id: String, - withdraw: Boolean, - ban: Boolean, - joinat: Date, - agreeOnTermsOfService: { type: Boolean, default: false }, //이용약관 동의 여부 - room: [Room], - subinfo: { - kaist: String, - sparcs: String, - facebook: String, - twitter: String, - }, - email: String, - __v: Number, -} -``` - -### `/agreeOnTermsOfService` **(POST)** - -- 이용 약관에 동의함 (철회 불가) - -#### URL Parameters, Request JSON form - -- 없음 - -#### Response - -- 200 "agree on Terms of Service successful" -- 400 "already agreed" -- 500 "internal server error" - -### `/getAgreeOnTermsOfService` **(GET)** - -- 이용 약관 동의 여부를 가져옴 - -#### URL Parameters, Request JSON form - -- 없음 - -#### Response - -```javascript -{ - agreeOnTermsOfService: Boolean -}, -``` - -### `/editNickname` **(POST)** - -- 해당 사용자의 닉네임을 새로 설정함. -- 새로운 닉네임은 상술한 규칙을 만족해야 함. - -#### URL Parameters - -- user_id : 사용자의 SPARCS SSO ID - -#### request JSON form - -```javascript -{ - nickname: String, // 새 닉네임 -} -``` - -#### Response - -```javascript -{ - status: 200, - data: "edit user nickname successful", -} -``` - -#### Errors - -- 400 "wrong nickname" -- 400 "such user id does not exist" -- 403 "not logged in" -- 500 "internal server error" - -### `/editProfileImg/getPUrl` **(POST)** - -- 프로필 이미지를 업로드할 수 있는 Presigned-url을 발급합니다. -- 프로필 사진은 아래 규칙을 만족해야 함. - 1. 파일 형식은 image/png, image/jpg, image/jpeg 중 하나 - 2. 파일 크기는 최대 50 MB - -#### URL Parameters - -- type : 업로드할 이미지 type - -#### request JSON form - -```javascript -{ - url: String, // pre-signed url - fields: Object, // post fields -} -``` - -#### Errors - -- 500 "internal server error" - -### `/editProfileImg/done` **(GET)** - -- 프로필 이미지가 S3에 정상적으로 업로드가 되었는지 확인합니다. - -#### URL Parameters - -- 없음 - -#### request JSON form - -```javascript -{ - result: Boolean, // 정상적으로 업로드 되었으면 true - profileImageUrl?: user._id, // 정상적으로 업로드 되었으면 새 프로필 이미지 파일명, 그렇지 않은 경우 undefined -} -``` - -#### Errors - -- 500 "internal server error" - -### `/` **(GET)** (for dev) - -- 사용자 전체 리스트를 반환함. - -#### URL Parameters - -- 없음 - -#### Response - -```javascript -{ - status: 200, - data: User[], // 전체 사용자 리스트 -} -``` - -#### Errors - -- 없음 - -### `/rooms` **(GET)** (for dev) - -- 사용자의 방 리스트를 반환함. - -#### URL Parameters - -- id : User document의 id - -#### Response - -```javascript -{ - id: String, // 요청된 id - rooms: Room[], // 방 리스트 -} -``` - -#### Errors - -- 404 "user/rooms : such id does not exist" -- 500 "user/rooms : internal server error" - -### `/:id` **(GET)** (for dev) - -- 사용자 정보를 반환함. - -#### URL Parameters - -- id : User document의 id - -#### Response - -```javascript -{ - status: 200, - data: User, //id에 대응되는 사용자 정보 -} -``` - -#### Errors - -- 404 "user/:id : such id does not exist" -- 500 "user/:id : internal server error" - -### `/:id/edit` **(POST)** (for dev) - -- 새 사용자 정보를 받아 업데이트함. - -#### URL Parameters - -- id : User document의 id - -#### request JSON form - -```javascript -User; //수정할 사용자 정보 -``` - -#### Response - -```javascript -{ - status: 200, - data: "edit user successful", -} -``` - -#### Errors - -- 400 "such id does not exist" - -### `/:id/ban` **(GET)** (for dev) - -- 해당 사용자를 밴함. - -#### URL Parameters - -- id : User document의 id - -#### Response - -```javascript -{ - status: 200, - data: "The user banned successfully", -} -``` - -#### Errors - -- 400 "The user does not exist" -- 409 "The user is already banned" -- 500 "User/ban : Error 500" - -### `/:id/unban` **(GET)** (for dev) - -- 해당 사용자를 밴 해제함. - -#### URL Parameters - -- id : User document의 id - -#### Response - -```javascript -{ - status: 200, - data: "The user unbanned successfully", -} -``` - -#### Errors - -- 400 "The user does not exist" -- 409 "The user is already banned" -- 500 "User/unban : Error 500" - -### `/:id/participate` **(POST)** (for dev) - -- 해당 사용자를 특정 방에 참여시킴. - -#### URL Parameters - -- id : User document의 id - -#### request JSON form - -```javascript -{ - room: String, // Room document의 id -} -``` - -#### Response - -```javascript -{ - status: 200, - data: "User/participate : Successful", -} -``` - -#### Errors - -- 400 "User/participate : Bad request" -- 400 "User/participate : No corresponding room" -- 400 "The user does not exist" -- 409 "The user already entered the room" -- 500 "User/participate : Error 500" diff --git a/src/routes/docs/utils.js b/src/routes/docs/utils.js new file mode 100644 index 00000000..b435476b --- /dev/null +++ b/src/routes/docs/utils.js @@ -0,0 +1,3 @@ +const objectIdPattern = `^[a-fA-F\d]{24}$`; + +module.exports = { objectIdPattern }; diff --git a/src/sampleGenerator/.gitignore b/src/sampleGenerator/.gitignore new file mode 100644 index 00000000..2909449b --- /dev/null +++ b/src/sampleGenerator/.gitignore @@ -0,0 +1,107 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# MongoDB Dump +dump/ diff --git a/src/sampleGenerator/README.md b/src/sampleGenerator/README.md new file mode 100644 index 00000000..5afd5960 --- /dev/null +++ b/src/sampleGenerator/README.md @@ -0,0 +1,28 @@ +# taxiSampleGenerator + +이 node 프로그램은 SPARCS-Taxi 프로젝트를 위한 샘플 사용자, 방, 채팅 목록을 생성합니다. +현재 이 프로그램으로 생성된 샘플 채팅 데이터는 입, 퇴장 메시지들과 일반 채팅 메시지들로만 구성되어 있습니다. + +**WARNING** +스크립트 실행 시 기존에 MongoDB에 저장된 사용자, 방, 채팅 정보는 **삭제**됩니다! + +**SETUP** + +1. *(optional)* Root directory의 `.env.test` 파일에 다음 내용을 추가합니다. + ``` + #방과 각각의 방의 채팅 개수 + SAMPLE_NUM_OF_ROOMS=2 + SAMPLE_NUM_OF_CHATS=200 + #채팅 간 최대 시간 간격(단위: 초, 소수도 가능) + SAMPLE_MAXIMUM_INTERVAL_BETWEEN_CHATS=20 + #새로운 채팅이 각각 입/퇴장 메시지일 확률(각각 10%) + SAMPLE_OCCURENCE_OF_JOIN=0.1 + SAMPLE_OCCURENCE_OF_ABORT=0.1 + ``` +1. sampleData.json에 장소, 유저, 방 데이터를 입력합니다. + javascript `User { "id": "sampleId", 사용자 id }` + +1. `pnpm start`로 샘플 채팅 데이터를 만들 수 있습니다. + +1. `pnpm run dumpDB`으로 현재 DB를 덤프할 수 있습니다. +1. `pnpm run restoreDB`로 과거 DB를 덤프 파일로부터 복원할 수 있습니다. diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js new file mode 100644 index 00000000..e83a9335 --- /dev/null +++ b/src/sampleGenerator/index.js @@ -0,0 +1,47 @@ +const { + generateUser, + generateRoom, + generateSampleLocations, + generateChats, +} = require("./src/testData"); +const { connectDatabase } = require("../modules/stores/mongo"); +const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); + +const database = connectDatabase(mongoUrl); + +const fs = require("fs"); +const sampleData = JSON.parse(fs.readFileSync("./sampleData.json")); + +const main = async () => { + await database.db.dropDatabase(); + + const { users, locations } = sampleData; + + const userOids = []; + const roomOids = []; + + for (const [index, user] of users.entries()) { + const userOid = await generateUser(user.id, index + 1, user.isAdmin); + userOids.push(userOid); + } + + const sampleLocationOids = await generateSampleLocations(locations); + + for (const index of Array(numberOfRooms).keys()) { + const roomOid = await generateRoom( + sampleLocationOids, + index + 1, + 7, + userOids[0] + ); //하드코딩: 일주일 뒤에 출발하는 방(들)을 만듭니다. + roomOids.push(roomOid); + } + + for (const roomOid of roomOids) { + await generateChats(roomOid, userOids, numberOfChats); + } + console.log("끝! 스크립트 실행을 중단하셔도 됩니다."); + process.exit(0); +}; + +database.on("open", main); diff --git a/src/sampleGenerator/loadenv.js b/src/sampleGenerator/loadenv.js new file mode 100644 index 00000000..0843789b --- /dev/null +++ b/src/sampleGenerator/loadenv.js @@ -0,0 +1,13 @@ +// Root directory에 있는 .env.test 파일을 읽어옴 +require("dotenv").config({ path: "../../.env.test" }); + +module.exports = { + mongo: process.env.DB_PATH, // required + numberOfRooms: parseInt(process.env.SAMPLE_NUM_OF_ROOMS ?? 2), // optional + numberOfChats: parseInt(process.env.SAMPLE_NUM_OF_CHATS ?? 200), // optional + maximumIntervalBtwChats: parseFloat( + process.env.SAMPLE_MAXIMUM_INTERVAL_BETWEEN_CHATS ?? 20 + ), // optional + occurenceOfJoin: parseFloat(process.env.SAMPLE_OCCURENCE_OF_JOIN ?? 0.1), // optional + occurenceOfAbort: parseFloat(process.env.SAMPLE_OCCURENCE_OF_ABORT ?? 0.1), // optional +}; diff --git a/src/sampleGenerator/package.json b/src/sampleGenerator/package.json new file mode 100644 index 00000000..3c7473bc --- /dev/null +++ b/src/sampleGenerator/package.json @@ -0,0 +1,17 @@ +{ + "name": "taxisamplegenerator", + "version": "1.0.0", + "description": "sample generator", + "main": "index.js", + "scripts": { + "preinstall": "npx only-allow pnpm", + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "dumpDB": "node tools/dump.js", + "restoreDB": "node tools/restore.js" + }, + "keywords": [ + "test" + ], + "license": "ISC" +} diff --git a/src/sampleGenerator/sampleData.json b/src/sampleGenerator/sampleData.json new file mode 100644 index 00000000..546812cd --- /dev/null +++ b/src/sampleGenerator/sampleData.json @@ -0,0 +1,112 @@ +{ + "users": [ + { + "id": "sunday", + "isAdmin": true + }, + { + "id": "monday", + "isAdmin": true + }, + { + "id": "tuesday", + "isAdmin": true + }, + { + "id": "wednesday", + "isAdmin": true + } + ], + "locations": [ + { + "koName": "택시승강장", + "enName": "Taxi Stand", + "longitude": 127.359507, + "latitude": 36.373199 + }, + { + "koName": "대전역", + "enName": "Daejeon Station", + "longitude": 127.434522, + "latitude": 36.331894 + }, + { + "koName": "갤러리아 타임월드", + "enName": "Galleria Timeworld", + "longitude": 127.378188, + "latitude": 36.351938 + }, + { + "koName": "궁동 로데오거리", + "enName": "Gung-dong Rodeo Street", + "longitude": 127.350161, + "latitude": 36.362785 + }, + { + "koName": "대전복합터미널", + "enName": "Daejeon Terminal Complex", + "longitude": 127.350161, + "latitude": 36.362785 + }, + { + "koName": "만년중학교", + "enName": "Mannyon Middle School", + "longitude": 127.375993, + "latitude": 36.366990 + }, + { + "koName": "서대전역", + "enName": "Seodaejeon Station", + "longitude": 127.403933, + "latitude": 36.322517 + }, + { + "koName": "신세계백화점", + "enName": "Shinsegae Department Store", + "longitude": 127.381905, + "latitude": 36.375168 + }, + { + "koName": "오리연못", + "enName": "Duck Pond", + "longitude": 127.362371, + "latitude": 36.367715 + }, + { + "koName": "월평역", + "enName": "Wolpyeong Station", + "longitude": 127.364352, + "latitude": 36.358271 + }, + { + "koName": "유성구청", + "enName": "Yuseong-gu Office", + "longitude": 127.356384, + "latitude": 36.362084 + }, + { + "koName": "유성 고속버스터미널", + "enName": "Yuseong Express Bus Terminal", + "longitude": 127.336467, + "latitude": 36.358279 + }, + { + "koName": "유성 시외버스터미널", + "enName": "Yuseong Intercity Bus Terminal", + "longitude": 127.335971, + "latitude": 36.355604 + }, + { + "koName": "대전청사 고속버스터미널", + "enName": "Government Complex Express Bus Terminal", + "longitude": 127.390504, + "latitude": 36.361462 + }, + { + "koName": "대전청사 시외버스터미널", + "enName": "Government Complex Intercity Bus Terminal", + "longitude": 127.379759, + "latitude": 36.361512 + } + ] +} diff --git a/src/sampleGenerator/src/testData.js b/src/sampleGenerator/src/testData.js new file mode 100644 index 00000000..1209c52a --- /dev/null +++ b/src/sampleGenerator/src/testData.js @@ -0,0 +1,199 @@ +const { + userModel, + roomModel, + locationModel, + chatModel, +} = require("../../modules/stores/mongo"); +const { generateProfileImageUrl } = require("../../modules/modifyProfile"); + +const { + maximumIntervalBtwChats, + occurenceOfJoin, + occurenceOfAbort, +} = require("../loadenv"); + +const generateUser = async (id, num, isAdmin) => { + const newUser = new userModel({ + id: id, + name: `${id}-name`, + nickname: `${id}-nickname`, + profileImageUrl: generateProfileImageUrl(), + joinat: Date.now(), + subinfo: { + kaist: new String(20230000 + num), // ^-^ + sparcs: "", + facebook: "", + twitter: "", + }, + email: `${id}@kaist.ac.kr`, + isAdmin: isAdmin, + }); + await newUser.save(); + return newUser._id; +}; + +const generateSampleLocations = async (locations) => { + if (locations.length === 0) { + console.log("Please provide location(s)!"); + } + + for (const location of locations) { + const locationDocument = new locationModel({ + koName: location.koName, + enName: location.enName, + longitude: location.longitude, + latitude: location.latitude, + }); + await locationDocument.save(); + } + + const locationDocuments = await locationModel.find().lean(); + return locationDocuments.map((locationDocument) => locationDocument._id); +}; + +const generateRoom = async (sampleLocationOids, num, daysAfter, creatorId) => { + const date = new Date(); + date.setDate(date.getDate() + daysAfter); + + let fromIdx = 0; + let toIdx = 0; + + while (fromIdx === toIdx) { + fromIdx = Math.floor(Math.random() * sampleLocationOids.length); + toIdx = Math.floor(Math.random() * sampleLocationOids.length); + } + + const newRoom = new roomModel({ + name: `test-${num}`, + from: sampleLocationOids[fromIdx], + to: sampleLocationOids[toIdx], + time: date, + part: [{ user: creatorId }], + madeat: Date.now(), + maxPartLength: 4, + }); + await newRoom.save(); + return newRoom._id; +}; + +const joinUserToRoom = async (userIdsInRoom, userIdsOutRoom, roomId) => { + // 들어올 사용자를 무작위로 선택 + const authorIdx = Math.floor(Math.random() * userIdsOutRoom.length); + const userOid = userIdsOutRoom[authorIdx]; + + // 방, 유저 상태 갱신 + userIdsInRoom.push(userOid); + userIdsOutRoom.splice(authorIdx, 1); + const user = await userModel.findById(userOid, "ongoingRoom"); + user.ongoingRoom.push(roomId); + await user.save(); + + return { userIdsInRoom, userIdsOutRoom, userOid }; +}; + +const abortUserfromRoom = async (userIdsInRoom, userIdsOutRoom, roomId) => { + // 나갈 사용자를 무작위로 선택 + const authorIdx = Math.floor(Math.random() * userIdsInRoom.length); + const userOid = userIdsInRoom[authorIdx]; + + // 방, 유저 상태 갱신 + userIdsOutRoom.push(userOid); + userIdsInRoom.splice(authorIdx, 1); + const user = await userModel.findById(userOid, "ongoingRoom"); + user.ongoingRoom.splice(user.ongoingRoom.indexOf(roomId), 1); + await user.save(); + + return { userIdsInRoom, userIdsOutRoom, userOid }; +}; + +const generateNormalChat = async (i, roomId, userOid, time) => { + const user = await userModel.findById(userOid); + const newChat = new chatModel({ + roomId: roomId, + type: "text", + authorId: user._id, + content: `안녕하세요! (${i}번째 메시지)`, + time: time, + inValid: false, + }); + await newChat.save(); +}; + +const generateJoinAbortChat = async (roomId, userOid, isJoining, time) => { + const user = await userModel.findById(userOid); + const newChat = new chatModel({ + roomId: roomId, + type: isJoining ? "in" : "out", + authorId: user._id, + content: user.id, + time: time, + isValid: false, + }); + await newChat.save(); +}; + +const generateChats = async (roomId, userOids, numOfChats) => { + const roomPopulateQuery = [{ path: "part", select: "id name nickname -_id" }]; + const room = await roomModel.findById(roomId).populate(roomPopulateQuery); + + let userIdsInRoom = []; + let userIdsOutRoom = userOids.map((userOid) => userOid); + let lastTime = Date.now(); + const maximumIntervalBtwChatsMilliseconds = 1000 * maximumIntervalBtwChats; + + for (const i of Array(numOfChats).keys()) { + lastTime += Math.floor(Math.random() * maximumIntervalBtwChatsMilliseconds); + const event = Math.random(); + + if ( + userIdsInRoom.length === 0 || + (event < occurenceOfJoin && userIdsOutRoom.length !== 0) + ) { + // 더 들어올 사용자가 있을 경우, 더 들어옴 + // 방, 유저 상태 갱신 + let userOid; + ({ userIdsInRoom, userIdsOutRoom, userOid } = await joinUserToRoom( + userIdsInRoom, + userIdsOutRoom, + roomId + )); + // 입장 메시지 생성 + await generateJoinAbortChat(roomId, userOid, true, lastTime); + } else if ( + occurenceOfJoin <= event && + event < occurenceOfJoin + occurenceOfAbort && + userIdsInRoom.length > 1 + ) { + // 나갈 사용자가 있을 경우, 나감 + // 방, 유저 상태 갱신 + let userOid; + ({ userIdsInRoom, userIdsOutRoom, userOid } = await abortUserfromRoom( + userIdsInRoom, + userIdsOutRoom, + roomId + )); + // 퇴장 메시지 생성 + await generateJoinAbortChat(roomId, userOid, false, lastTime); + } else { + // 방이 비어있지 않을 경우, 일반 채팅 메시지를 만듦 + if (userIdsInRoom.length !== 0) { + const authorIdx = Math.floor(Math.random() * userIdsInRoom.length); + const user = userIdsInRoom[authorIdx]; + await generateNormalChat(i, roomId, user, lastTime); + } + } + } + // 현재 참여중인 사용자 기준으로 방의 part 리스트를 업데이트함 + room.part = userIdsInRoom.map((userOid) => { + return { user: userOid }; + }); + await room.save(); + return; +}; + +module.exports = { + generateUser, + generateRoom, + generateSampleLocations, + generateChats, +}; diff --git a/src/sampleGenerator/tools/dump.js b/src/sampleGenerator/tools/dump.js new file mode 100644 index 00000000..7d26d802 --- /dev/null +++ b/src/sampleGenerator/tools/dump.js @@ -0,0 +1,20 @@ +const util = require("util"); +const path = require("path"); +const exec = util.promisify(require("child_process").exec); +const { mongo: mongoUrl } = require("../loadenv"); + +const main = async () => { + const { stdout, stderr } = await exec( + `mongodump ${mongoUrl} --out ${path.resolve("dump")}` + ); + console.log("dump 디렉토리에 데이터베이스 데이터를 덤프했습니다."); + process.exit(0); +}; + +try { + main(); +} catch { + console.log( + "DB 연결 주소가 올바르지 않습니다. DB 연결 주소를 다시 한 번 확인해주세요." + ); +} diff --git a/src/sampleGenerator/tools/restore.js b/src/sampleGenerator/tools/restore.js new file mode 100644 index 00000000..5c14c98b --- /dev/null +++ b/src/sampleGenerator/tools/restore.js @@ -0,0 +1,23 @@ +const util = require("util"); +const path = require("path"); +const exec = util.promisify(require("child_process").exec); +const { mongo: mongoUrl } = require("../loadenv"); + +const main = async () => { + const dbName = mongoUrl.split("/").pop(); + const { stdout, stderr } = await exec( + `mongorestore ${mongoUrl} ${path.resolve("dump", dbName)}` + ); + console.log( + "dump 디렉토리로부터 데이터베이스 정보를 성공적으로 복원했습니다." + ); + process.exit(0); +}; + +try { + main(); +} catch { + console.log( + "DB를 덤프해올 디렉토리가 존재하지 않습니다. 경로를 다시 한 번 확인해주세요." + ); +} diff --git a/src/services/chats.js b/src/services/chats.js index 921c4946..c21f4b75 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -273,7 +273,7 @@ const uploadChatImgDoneHandler = async (req, res) => { if (!user) { return res .status(500) - .send("Chat/uploadChatImg/getPUrl : internal server error"); + .send("Chat/uploadChatImg/done : internal server error"); } if (!chat) { return res.status(404).json({ @@ -294,7 +294,7 @@ const uploadChatImgDoneHandler = async (req, res) => { if (err) { return res .status(500) - .send("Chat/uploadChatImg/getPUrl : internal server error"); + .send("Chat/uploadChatImg/done : internal server error"); } chat.content = chat._id; diff --git a/src/services/users.js b/src/services/users.js index 73f79dde..d3adde6f 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -18,13 +18,13 @@ const agreeOnTermsOfServiceHandler = async (req, res) => { res .status(200) .send( - "User/agreeOnTermsOfService : agree on Terms of Service successful" + "Users/agreeOnTermsOfService : agree on Terms of Service successful" ); } else { - res.status(400).send("User/agreeOnTermsOfService : already agreed"); + res.status(400).send("Users/agreeOnTermsOfService : already agreed"); } } catch { - res.status(500).send("User/agreeOnTermsOfService : internal server error"); + res.status(500).send("Users/agreeOnTermsOfService : internal server error"); } }; @@ -36,7 +36,9 @@ const getAgreeOnTermsOfServiceHandler = async (req, res) => { const agreeOnTermsOfService = user.agreeOnTermsOfService === true; res.json({ agreeOnTermsOfService }); } catch { - res.status(500).send("/getAgreeOnTermsOfService : internal server error"); + res + .status(500) + .send("Users/getAgreeOnTermsOfService : internal server error"); } }; @@ -55,13 +57,15 @@ const editNicknameHandler = async (req, res) => { req.timestamp ); - res.status(200).send("User/editNickname : edit user nickname successful"); + res + .status(200) + .send("Users/editNickname : edit user nickname successful"); } else { - res.status(400).send("User/editNickname : such user id does not exist"); + res.status(400).send("Users/editNickname : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("User/editNickname : internal server error"); + res.status(500).send("Users/editNickname : internal server error"); } }; @@ -81,13 +85,13 @@ const editAccountHandler = async (req, res) => { newAccount ); - res.status(200).send("User/editAccount : edit user account successful"); + res.status(200).send("Users/editAccount : edit user account successful"); } else { - res.status(400).send("User/editAccount : such user id does not exist"); + res.status(400).send("Users/editAccount : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("User/editAccount : internal server error"); + res.status(500).send("Users/editAccount : internal server error"); } }; @@ -98,14 +102,14 @@ const editProfileImgGetPUrlHandler = async (req, res) => { if (!user) { return res .status(500) - .send("User/editProfileImg/getPUrl : internal server error"); + .send("Users/editProfileImg/getPUrl : internal server error"); } const key = `profile-img/${user._id}`; aws.getUploadPUrlPost(key, type, (err, data) => { if (err) { return res .status(500) - .send("User/editProfileImg/getPUrl : internal server error"); + .send("Users/editProfileImg/getPUrl : internal server error"); } data.fields["Content-Type"] = type; data.fields["key"] = key; @@ -115,7 +119,9 @@ const editProfileImgGetPUrlHandler = async (req, res) => { }); }); } catch (e) { - res.status(500).send("User/editProfileImg/getPUrl : internal server error"); + res + .status(500) + .send("Users/editProfileImg/getPUrl : internal server error"); } }; @@ -125,7 +131,7 @@ const editProfileImgDoneHandler = async (req, res) => { if (!user) { return res .status(500) - .send("User/editProfileImg/done : internal server error"); + .send("Users/editProfileImg/done : internal server error"); } const key = `profile-img/${user._id}`; aws.foundObject(key, async (err) => { @@ -133,25 +139,25 @@ const editProfileImgDoneHandler = async (req, res) => { logger.error(err); return res .status(500) - .send("User/editProfileImg/done : internal server error"); + .send("Users/editProfileImg/done : internal server error"); } const userAfter = await userModel.findOneAndUpdate( { id: req.userId }, - { profileImageUrl: user._id }, + { profileImageUrl: aws.getS3Url(`/${key}?token=${req.timestamp}`) }, { new: true } ); if (!userAfter) { return res .status(500) - .send("User/editProfileImg/done : internal server error"); + .send("Users/editProfileImg/done : internal server error"); } res.json({ result: true, - profileImageUrl: userAfter._id, + profileImageUrl: userAfter.profileImageUrl, }); }); } catch (e) { - res.status(500).send("User/editProfileImg/done : internal server error"); + res.status(500).send("Users/editProfileImg/done : internal server error"); } }; @@ -165,11 +171,13 @@ const resetNicknameHandler = async (req, res) => { if (!result) return res .status(400) - .send("User/resetNickname : such user does not exist"); - res.status(200).send("User/resetNickname : reset user nickname successful"); + .send("Users/resetNickname : such user does not exist"); + res + .status(200) + .send("Users/resetNickname : reset user nickname successful"); } catch (err) { logger.error(err); - res.status(500).send("User/resetNickname : internal server error"); + res.status(500).send("Users/resetNickname : internal server error"); } }; @@ -183,12 +191,12 @@ const resetProfileImgHandler = async (req, res) => { if (!result) return res .status(400) - .send("User/resetProfileImg : such user does not exist"); + .send("Users/resetProfileImg : such user does not exist"); res .status(200) - .send("User/resetProfileImg : reset user profile image successful"); + .send("Users/resetProfileImg : reset user profile image successful"); } catch (err) { - res.status(500).send("User/resetProfileImg : internal server error"); + res.status(500).send("Users/resetProfileImg : internal server error"); } }; diff --git a/test/services/users.js b/test/services/users.js index 1bb2f586..4f2195da 100644 --- a/test/services/users.js +++ b/test/services/users.js @@ -15,7 +15,7 @@ describe("[users] 1.agreeOnTermsOfServiceHandler", () => { it("should return correct response from handler", async () => { const testUser1 = await userGenerator("test1", testData); const msg = - "User/agreeOnTermsOfService : agree on Terms of Service successful"; + "Users/agreeOnTermsOfService : agree on Terms of Service successful"; let req = httpMocks.createRequest({ userId: testUser1.id, }); @@ -50,7 +50,7 @@ describe("[users] 3.editNicknameHandler", () => { it("should return correct response from handler", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); - const msg = "User/editNickname : edit user nickname successful"; + const msg = "Users/editNickname : edit user nickname successful"; let req = httpMocks.createRequest({ userId: testUser1.id, body: { @@ -77,7 +77,7 @@ describe("[users] 4.editAccountHandler", () => { it("should return correct response from handler", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); - const msg = "User/editAccount : edit user account successful"; + const msg = "Users/editAccount : edit user account successful"; let req = httpMocks.createRequest({ userId: testUser1.id, body: { diff --git a/test/utils.js b/test/utils.js index b3920e83..e537913b 100644 --- a/test/utils.js +++ b/test/utils.js @@ -7,8 +7,9 @@ const { connectDatabase, } = require("../src/modules/stores/mongo"); const { generateProfileImageUrl } = require("../src/modules/modifyProfile"); +const { mongo: mongoUrl } = require("../loadenv"); -connectDatabase(); +connectDatabase(mongoUrl); // 테스트를 위한 유저 생성 함수 const userGenerator = async (username, testData) => {