├── .dockerignore
├── .eslintrc.js
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── deploy.sh
├── docker-compose.yml
├── nest-cli.json
├── nginx
├── Dockerfile
└── nginx.conf
├── package-lock.json
├── package.json
├── src
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── auth
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── dto
│ │ └── requests
│ │ │ ├── createKakaoUser.dto.ts
│ │ │ └── updateKakaoUser.dto.ts
│ ├── strategy
│ │ └── jwt.strategy.ts
│ └── test
│ │ ├── auth.controller.spec.ts
│ │ └── auth.service.spec.ts
├── common
│ ├── decorators
│ │ ├── pagination.decorators.ts
│ │ └── user.decorators.ts
│ ├── exceptions
│ │ └── httpExceptionFilter.ts
│ ├── guards
│ │ └── jwtAuth.guard.ts
│ ├── middlewares
│ │ └── logger.middleware.ts
│ └── paginations
│ │ ├── pagination.request.ts
│ │ ├── pagination.response.ts
│ │ ├── pagination.swagger.ts
│ │ └── paginationBuilder.response.ts
├── domain
│ ├── friend
│ │ ├── dto
│ │ │ └── response
│ │ │ │ └── responseFriend.dto.ts
│ │ ├── entities
│ │ │ └── friend.entity.ts
│ │ ├── friend.controller.ts
│ │ ├── friend.module.ts
│ │ └── friend.service.ts
│ ├── kakao
│ │ ├── kakao.module.ts
│ │ ├── kakao.service.ts
│ │ ├── kakaoToken.ts
│ │ └── repository
│ │ │ └── kakaoToken.memory.repository.ts
│ ├── letter
│ │ ├── dto
│ │ │ ├── requests
│ │ │ │ ├── createDraftLetter.request.dto.ts
│ │ │ │ ├── createExternalLetter.request.dto.ts
│ │ │ │ ├── createExternalLetterImg.request.dto.ts
│ │ │ │ ├── findAllLetter.request.dto.ts
│ │ │ │ └── kakaoCallback.request.dto.ts
│ │ │ └── responses
│ │ │ │ ├── letterDetail.response.dto.ts
│ │ │ │ ├── letterStorage.response.dto.ts
│ │ │ │ └── receviedTempLetter.response.dto.ts
│ │ ├── entities
│ │ │ ├── letterBody.entity.ts
│ │ │ ├── receivedLetter.entity.ts
│ │ │ └── sendLetter.entity.ts
│ │ ├── letter.constants.ts
│ │ ├── letter.controller.ts
│ │ ├── letter.module.ts
│ │ ├── letter.received.service.ts
│ │ ├── letter.sent.controller.ts
│ │ ├── letter.sent.service.ts
│ │ ├── letter.service.ts
│ │ ├── letter.utils.ts
│ │ ├── lettter.received.controller.ts
│ │ ├── repository
│ │ │ └── tempLetter.repository.ts
│ │ └── test
│ │ │ ├── letter.controller.spec.ts
│ │ │ └── letter.service.spec.ts
│ ├── notice
│ │ ├── dto
│ │ │ ├── createNotice.dto.ts
│ │ │ ├── deleteNotice.dto.ts
│ │ │ └── updateNotice.dto.ts
│ │ ├── entities
│ │ │ └── notice.entity.ts
│ │ ├── notice.controller.ts
│ │ ├── notice.module.ts
│ │ ├── notice.service.ts
│ │ └── test
│ │ │ ├── notice.controller.spec.ts
│ │ │ └── notice.service.spec.ts
│ ├── relationship
│ │ └── entities
│ │ │ └── relationship.entity.ts
│ ├── reminder
│ │ ├── dto
│ │ │ ├── requests
│ │ │ │ ├── createReminder.request.dto.ts
│ │ │ │ ├── findAllReminder.request.dto.ts
│ │ │ │ └── updateReminder.request.dto.ts
│ │ │ └── responses
│ │ │ │ ├── reminder.response.dto.ts
│ │ │ │ └── reminderStatus.response.dto.ts
│ │ ├── entities
│ │ │ └── reminder.entity.ts
│ │ ├── reminder.controller.ts
│ │ ├── reminder.module.ts
│ │ ├── reminder.service.ts
│ │ └── test
│ │ │ ├── reminder.controller.spec.ts
│ │ │ └── reminder.service.spec.ts
│ ├── reply
│ │ ├── dtos
│ │ │ └── requests
│ │ │ │ └── createReply.request.dto.ts
│ │ ├── entities
│ │ │ └── reply.entity.ts
│ │ ├── reply.controller.ts
│ │ ├── reply.module.ts
│ │ └── reply.service.ts
│ ├── sentence
│ │ ├── dto
│ │ │ ├── requests
│ │ │ │ └── createSentence.request.dto.ts
│ │ │ └── responses
│ │ │ │ ├── manysentence.response.dto.ts
│ │ │ │ └── sentence.response.dto.ts
│ │ ├── entities
│ │ │ └── sentence.entity.ts
│ │ ├── sentence.constant.ts
│ │ ├── sentence.controller.ts
│ │ ├── sentence.module.ts
│ │ ├── sentence.service.ts
│ │ └── test
│ │ │ ├── sentence.controller.spec.ts
│ │ │ └── sentence.service.spec.ts
│ └── users
│ │ ├── dto
│ │ ├── requests
│ │ │ └── updateUser.dto.ts
│ │ └── response
│ │ │ └── user.response.dto.ts
│ │ ├── entities
│ │ ├── social.entity.ts
│ │ ├── user.entity.ts
│ │ └── userInfo.entity.ts
│ │ ├── users.controller.spec.ts
│ │ ├── users.controller.ts
│ │ ├── users.module.ts
│ │ ├── users.service.spec.ts
│ │ └── users.service.ts
├── infrastructor
│ ├── S3
│ │ └── s3.multer.ts
│ ├── document
│ │ └── document.swagger.ts
│ └── monitoring
│ │ └── monitoring.telegram.ts
└── main.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | node_modules
4 | dist
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test & Test Coverage CI
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 | pull_request:
7 | branches: ['master']
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | cache: 'npm'
24 | - run: npm i
25 | continue-on-error: true
26 | - run: npm run format
27 | continue-on-error: true
28 | - run: npm test
29 | continue-on-error: true
30 | - run: npm run test:cov
31 | continue-on-error: true
32 |
33 | - name: Coveralls
34 | uses: coverallsapp/github-action@master
35 | continue-on-error: true
36 | with:
37 | github-token: ${{ secrets.GITHUB_TOKEN }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff:
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 | .idea/dictionaries
10 |
11 | # Sensitive or high-churn files:
12 | .idea/**/dataSources/
13 | .idea/**/dataSources.ids
14 | .idea/**/dataSources.xml
15 | .idea/**/dataSources.local.xml
16 | .idea/**/sqlDataSources.xml
17 | .idea/**/dynamic.xml
18 | .idea/**/uiDesigner.xml
19 |
20 | # Gradle:
21 | .idea/**/gradle.xml
22 | .idea/**/libraries
23 |
24 | # CMake
25 | cmake-build-debug/
26 |
27 | # Mongo Explorer plugin:
28 | .idea/**/mongoSettings.xml
29 |
30 | ## File-based project format:
31 | *.iws
32 |
33 | ## Plugin-specific files:
34 |
35 | # IntelliJ
36 | out/
37 |
38 | # mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # Cursive Clojure plugin
45 | .idea/replstate.xml
46 |
47 | # Crashlytics plugin (for Android Studio and IntelliJ)
48 | com_crashlytics_export_strings.xml
49 | crashlytics.properties
50 | crashlytics-build.properties
51 | fabric.properties
52 | ### VisualStudio template
53 | ## Ignore Visual Studio temporary files, build results, and
54 | ## files generated by popular Visual Studio add-ons.
55 | ##
56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
57 |
58 | # User-specific files
59 | *.suo
60 | *.user
61 | *.userosscache
62 | *.sln.docstates
63 |
64 | # User-specific files (MonoDevelop/Xamarin Studio)
65 | *.userprefs
66 |
67 | # Build results
68 | [Dd]ebug/
69 | [Dd]ebugPublic/
70 | [Rr]elease/
71 | [Rr]eleases/
72 | x64/
73 | x86/
74 | bld/
75 | [Bb]in/
76 | [Oo]bj/
77 | [Ll]og/
78 |
79 | # Visual Studio 2015 cache/options directory
80 | .vs/
81 | # Uncomment if you have tasks that create the project's static files in wwwroot
82 | #wwwroot/
83 |
84 | # MSTest test Results
85 | [Tt]est[Rr]esult*/
86 | [Bb]uild[Ll]og.*
87 |
88 | # NUNIT
89 | *.VisualState.xml
90 | TestResult.xml
91 |
92 | # Build Results of an ATL Project
93 | [Dd]ebugPS/
94 | [Rr]eleasePS/
95 | dlldata.c
96 |
97 | # Benchmark Results
98 | BenchmarkDotNet.Artifacts/
99 |
100 | # .NET Core
101 | project.lock.json
102 | project.fragment.lock.json
103 | artifacts/
104 | **/Properties/launchSettings.json
105 |
106 | *_i.c
107 | *_p.c
108 | *_i.h
109 | *.ilk
110 | *.meta
111 | *.obj
112 | *.pch
113 | *.pdb
114 | *.pgc
115 | *.pgd
116 | *.rsp
117 | *.sbr
118 | *.tlb
119 | *.tli
120 | *.tlh
121 | *.tmp
122 | *.tmp_proj
123 | *.log
124 | *.vspscc
125 | *.vssscc
126 | .builds
127 | *.pidb
128 | *.svclog
129 | *.scc
130 |
131 | # Chutzpah Test files
132 | _Chutzpah*
133 |
134 | # Visual C++ cache files
135 | ipch/
136 | *.aps
137 | *.ncb
138 | *.opendb
139 | *.opensdf
140 | *.sdf
141 | *.cachefile
142 | *.VC.db
143 | *.VC.VC.opendb
144 |
145 | # Visual Studio profiler
146 | *.psess
147 | *.vsp
148 | *.vspx
149 | *.sap
150 |
151 | # Visual Studio Trace Files
152 | *.e2e
153 |
154 | # TFS 2012 Local Workspace
155 | $tf/
156 |
157 | # Guidance Automation Toolkit
158 | *.gpState
159 |
160 | # ReSharper is a .NET coding add-in
161 | _ReSharper*/
162 | *.[Rr]e[Ss]harper
163 | *.DotSettings.user
164 |
165 | # JustCode is a .NET coding add-in
166 | .JustCode
167 |
168 | # TeamCity is a build add-in
169 | _TeamCity*
170 |
171 | # DotCover is a Code Coverage Tool
172 | *.dotCover
173 |
174 | # AxoCover is a Code Coverage Tool
175 | .axoCover/*
176 | !.axoCover/settings.json
177 |
178 | # Visual Studio code coverage results
179 | *.coverage
180 | *.coveragexml
181 |
182 | # NCrunch
183 | _NCrunch_*
184 | .*crunch*.local.xml
185 | nCrunchTemp_*
186 |
187 | # MightyMoose
188 | *.mm.*
189 | AutoTest.Net/
190 |
191 | # Web workbench (sass)
192 | .sass-cache/
193 |
194 | # Installshield output folder
195 | [Ee]xpress/
196 |
197 | # DocProject is a documentation generator add-in
198 | DocProject/buildhelp/
199 | DocProject/Help/*.HxT
200 | DocProject/Help/*.HxC
201 | DocProject/Help/*.hhc
202 | DocProject/Help/*.hhk
203 | DocProject/Help/*.hhp
204 | DocProject/Help/Html2
205 | DocProject/Help/html
206 |
207 | # Click-Once directory
208 | publish/
209 |
210 | # Publish Web Output
211 | *.[Pp]ublish.xml
212 | *.azurePubxml
213 | # Note: Comment the next line if you want to checkin your web deploy settings,
214 | # but database connection strings (with potential passwords) will be unencrypted
215 | *.pubxml
216 | *.publishproj
217 |
218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
219 | # checkin your Azure Web App publish settings, but sensitive information contained
220 | # in these scripts will be unencrypted
221 | PublishScripts/
222 |
223 | # NuGet Packages
224 | *.nupkg
225 | # The packages folder can be ignored because of Package Restore
226 | **/[Pp]ackages/*
227 | # except build/, which is used as an MSBuild target.
228 | !**/[Pp]ackages/build/
229 | # Uncomment if necessary however generally it will be regenerated when needed
230 | #!**/[Pp]ackages/repositories.config
231 | # NuGet v3's project.json files produces more ignorable files
232 | *.nuget.props
233 | *.nuget.targets
234 |
235 | # Microsoft Azure Build Output
236 | csx/
237 | *.build.csdef
238 |
239 | # Microsoft Azure Emulator
240 | ecf/
241 | rcf/
242 |
243 | # Windows Store app package directories and files
244 | AppPackages/
245 | BundleArtifacts/
246 | Package.StoreAssociation.xml
247 | _pkginfo.txt
248 | *.appx
249 |
250 | # Visual Studio cache files
251 | # files ending in .cache can be ignored
252 | *.[Cc]ache
253 | # but keep track of directories ending in .cache
254 | !*.[Cc]ache/
255 |
256 | # Others
257 | ClientBin/
258 | ~$*
259 | *~
260 | *.dbmdl
261 | *.dbproj.schemaview
262 | *.jfm
263 | *.pfx
264 | *.publishsettings
265 | orleans.codegen.cs
266 |
267 | # Since there are multiple workflows, uncomment next line to ignore bower_components
268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
269 | #bower_components/
270 |
271 | # RIA/Silverlight projects
272 | Generated_Code/
273 |
274 | # Backup & report files from converting an old project file
275 | # to a newer Visual Studio version. Backup files are not needed,
276 | # because we have git ;-)
277 | _UpgradeReport_Files/
278 | Backup*/
279 | UpgradeLog*.XML
280 | UpgradeLog*.htm
281 |
282 | # SQL Server files
283 | *.mdf
284 | *.ldf
285 | *.ndf
286 |
287 | # Business Intelligence projects
288 | *.rdl.data
289 | *.bim.layout
290 | *.bim_*.settings
291 |
292 | # Microsoft Fakes
293 | FakesAssemblies/
294 |
295 | # GhostDoc plugin setting file
296 | *.GhostDoc.xml
297 |
298 | # Node.js Tools for Visual Studio
299 | .ntvs_analysis.dat
300 | node_modules/
301 |
302 | # Typescript v1 declaration files
303 | typings/
304 |
305 | # Visual Studio 6 build log
306 | *.plg
307 |
308 | # Visual Studio 6 workspace options file
309 | *.opt
310 |
311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
312 | *.vbw
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # JetBrains Rider
330 | .idea/
331 | *.sln.iml
332 |
333 | # IDE - VSCode
334 | .vscode/*
335 | !.vscode/settings.json
336 | !.vscode/tasks.json
337 | !.vscode/launch.json
338 | !.vscode/extensions.json
339 |
340 | # CodeRush
341 | .cr/
342 |
343 | # Python Tools for Visual Studio (PTVS)
344 | __pycache__/
345 | *.pyc
346 |
347 | # Cake - Uncomment if you are using it
348 | # tools/**
349 | # !tools/packages.config
350 |
351 | # Tabs Studio
352 | *.tss
353 |
354 | # Telerik's JustMock configuration file
355 | *.jmconfig
356 |
357 | # BizTalk build output
358 | *.btp.cs
359 | *.btm.cs
360 | *.odx.cs
361 | *.xsd.cs
362 |
363 | # OpenCover UI analysis results
364 | OpenCover/
365 | coverage/
366 |
367 | ### macOS template
368 | # General
369 | .DS_Store
370 | .AppleDouble
371 | .LSOverride
372 |
373 | # Icon must end with two \r
374 | Icon
375 |
376 | # Thumbnails
377 | ._*
378 |
379 | # Files that might appear in the root of a volume
380 | .DocumentRevisions-V100
381 | .fseventsd
382 | .Spotlight-V100
383 | .TemporaryItems
384 | .Trashes
385 | .VolumeIcon.icns
386 | .com.apple.timemachine.donotpresent
387 |
388 | # Directories potentially created on remote AFP share
389 | .AppleDB
390 | .AppleDesktop
391 | Network Trash Folder
392 | Temporary Items
393 | .apdisk
394 |
395 | =======
396 | # Local
397 | .env
398 | dist
399 |
400 | # Logs
401 | logs
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18
2 |
3 | WORKDIR /usr/src/app
4 | COPY package*.json ./
5 |
6 | ENV TZ=Asia/Seoul
7 |
8 | RUN apt-get update
9 | RUN apt-get install tzdata
10 | RUN npm install -g pm2
11 | RUN npm install
12 |
13 | COPY . ./
14 |
15 | RUN npm run build
16 |
17 | EXPOSE 3000
18 | CMD ["npm", "run", "start:prod"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | 
4 |
5 |
6 | ## ✉️ 꼬깃 접어 전하는 마음, ggo-geet ✉️
7 |
8 | 말로 전하기 어려운 마음을 즐겁게 전할 수 있게 해주는 편지 서비스
9 |
10 | 
11 |
12 | 소중한 사람에게 마음을 표현하고 싶을 때, 어떻게 전하고 있나요?
13 |
14 | 
15 |
16 | 번거롭고 귀찮아서, 할 말이 없어서, 뭐라고 적을지 몰라서 등…
17 |
18 | 살아오며 더 이상 편지를 쓰지 않게 된 이유는 아주 많습니다.
19 |
20 |
21 |
22 | 📤 **꼬깃 보내기**
23 |
24 | > **전하고 싶은 마음을 적어보세요.**
25 |
26 | 
27 |
28 | 
29 |
30 | 마음을 전하는 상황을 선택해주세요.
31 |
32 | 상황에 맞는 다양한 종이 친구들과 함께, 맞춤형 가이드가 제공됩니다.
33 |
34 | 
35 |
36 | ✍️ **꼬깃 가이드**
37 | >
38 | >
39 | > 꼬깃을 작성할 때, 사이드와 메모를 통해서 작성에 도움을 받을 수 있어요.
40 | >
41 | > **상황과 유저에 맞는 적합한 문장이 추천**됩니다.
42 | >
43 |
44 | 
45 |
46 | 작성한 꼬깃은 카카오톡으로 공유되며,
47 |
48 | 회원과 비회원 모두 카카오톡을 통해서 편지를 확인할 수 있습니다.
49 |
50 |
51 | 📭 **꼬깃 보관함**
52 |
53 | >**주고받은 마음을 분류해 보관하세요.**
54 |
55 | 
56 |
57 | 서비스 내에서 주고 받은 모든 편지와,
58 |
59 | 외부에서 받은 편지를 추가할 수 있습니다.
60 |
61 |
62 | 📝 **꼬깃 메모**
63 |
64 | > **전하려는 마음을 잊지 마세요.**
65 |
66 | 
67 |
68 |
69 |
70 | ## 💻Web Site
71 |
72 | www.ggo-geet.com
73 |
74 | ### Repository
75 |
76 | **Frontend**
77 |
78 | [Github](https://github.com/depromeet/ggogeet-client)
79 |
80 | **Backend**
81 |
82 | [Github](https://github.com/depromeet/ggogeet_backend)
83 |
84 |
85 | ## 🔧 Skill
86 | ### Backend
87 |
88 | - **Stack**
89 |
90 | 
91 |
92 |
93 |
94 | - **System Architecture**
95 |
96 | 
97 |
98 | **📂 Directory structure**
99 |
100 | ```
101 | ├── nginx
102 | ├── logs
103 | ├── src/
104 | │ ├── auth/
105 | │ │ ├── dto
106 | │ │ └── strategy
107 | │ ├── common/
108 | │ │ ├── decorators/
109 | │ │ ├── exceptions/
110 | │ │ ├── guards/
111 | │ │ ├── middlewares/
112 | │ │ ├── paginations/
113 | │ │ └── pipes/
114 | │ ├── constants
115 | │ ├── friend/
116 | │ ├── kakao/
117 | │ ├── letter/
118 | │ │ ├── dto/
119 | │ │ │ ├── requests/
120 | │ │ │ └── reponses/
121 | │ │ ├── entities/
122 | │ │ ├── repository/
123 | │ │ ├── letter.module.ts
124 | │ │ ├── letter.controller.ts
125 | │ │ └── letter.service.ts
126 | │ ├── notice/
127 | │ ├── relationship/
128 | │ ├── letter/
129 | │ ├── notice/
130 | │ ├── relationship/
131 | │ ├── reminders/
132 | │ ├── reply/
133 | │ ├── sentence/
134 | │ ├── users/
135 | │ ├── utils/
136 | │ │ ├── index.ts
137 | │ │ ├── s3.multer.ts
138 | │ └ └── document.swagger.ts
139 | ├── test/
140 | ├── deploy.sh
141 | ├── docker-compose.yml
142 | ├── Dockerfile
143 | └── README.md
144 |
145 | ```
146 |
147 | ## 🫶 Owner
148 |
149 |
260 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd ~/ggogeet_backend
4 | git pull origin master
5 | sudo docker-compose up --build -d
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 |
3 | services:
4 | backend:
5 | container_name: backend
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | ports:
10 | - "3000:3000"
11 | volumes:
12 | - './logs:/usr/src/app/logs'
13 | stdin_open: true
14 | tty: true
15 | depends_on:
16 | - redis
17 | redis:
18 | image: redis
19 | command: redis-server --port 6379
20 | container_name: redis
21 | hostname: redis
22 | labels:
23 | - "name=redis"
24 | - "mode=standalone"
25 | ports:
26 | - 6379:6379
27 | nginx:
28 | container_name: nginx
29 | build:
30 | context: ./nginx
31 | dockerfile: Dockerfile
32 | restart: always
33 | ports:
34 | - "80:80"
35 | - "443:443"
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 |
3 | RUN rm /etc/nginx/conf.d/default.conf
4 |
5 | COPY nginx.conf /etc/nginx/conf.d/
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80 default_server;
3 | listen [::]:80 default_server;
4 |
5 | root /var/www/html;
6 |
7 | index index.html index.htm index.nginx-debian.html;
8 |
9 | server_name _;
10 |
11 | location / {
12 | proxy_pass http://backend:3000/;
13 | }
14 |
15 | }
16 |
17 | client_max_body_size 50M;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-typescript-starter",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Nest TypeScript starter repository",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "nest build",
9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
10 | "start": "nest start",
11 | "start:dev": "nest start --watch",
12 | "start:debug": "nest start --debug --watch",
13 | "start:prod": "pm2-runtime start dist/main.js",
14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
15 | "test": "jest",
16 | "test:watch": "jest --watch",
17 | "test:cov": "jest --coverage",
18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
19 | "test:e2e": "jest --config ./test/jest-e2e.json",
20 | "coveralls": "jest --coverage && coveralls < coverage/lcov.info"
21 | },
22 | "dependencies": {
23 | "@aws-sdk/client-s3": "^3.222.0",
24 | "@liaoliaots/nestjs-redis": "^9.0.5",
25 | "@nestjs/common": "^9.0.0",
26 | "@nestjs/config": "^2.2.0",
27 | "@nestjs/core": "^9.0.0",
28 | "@nestjs/jwt": "^9.0.0",
29 | "@nestjs/mapped-types": "*",
30 | "@nestjs/passport": "^9.0.0",
31 | "@nestjs/platform-express": "^9.0.0",
32 | "@nestjs/swagger": "^6.1.4",
33 | "@nestjs/typeorm": "^9.0.1",
34 | "@types/passport-jwt": "^3.0.7",
35 | "@types/passport-kakao": "^1.0.0",
36 | "axios": "^1.2.1",
37 | "class-transformer": "^0.5.1",
38 | "class-validator": "^0.13.2",
39 | "ioredis": "^5.2.4",
40 | "multer-s3": "^3.0.1",
41 | "mysql2": "^2.3.3",
42 | "nest-winston": "^1.8.0",
43 | "passport": "^0.6.0",
44 | "passport-jwt": "^4.0.0",
45 | "passport-kakao": "^1.0.1",
46 | "reflect-metadata": "^0.1.13",
47 | "rxjs": "^7.5.5",
48 | "swagger-ui-express": "^4.6.0",
49 | "typeorm": "^0.3.10",
50 | "winston": "^3.8.2",
51 | "winston-daily-rotate-file": "^4.7.1"
52 | },
53 | "devDependencies": {
54 | "@nestjs/cli": "^9.0.0",
55 | "@nestjs/schematics": "^9.0.0",
56 | "@nestjs/testing": "^9.2.0",
57 | "@types/express": "^4.17.13",
58 | "@types/jest": "^28.1.4",
59 | "@types/multer-s3": "^3.0.0",
60 | "@types/node": "^18.0.3",
61 | "@types/supertest": "^2.0.12",
62 | "@typescript-eslint/eslint-plugin": "^5.30.5",
63 | "@typescript-eslint/parser": "^5.30.5",
64 | "coveralls": "^3.1.1",
65 | "eslint": "^8.19.0",
66 | "eslint-config-prettier": "^8.5.0",
67 | "eslint-plugin-prettier": "^4.2.1",
68 | "jest": "^28.1.3",
69 | "prettier": "^2.7.1",
70 | "source-map-support": "^0.5.21",
71 | "supertest": "^6.2.4",
72 | "ts-jest": "^28.0.5",
73 | "ts-loader": "^9.3.1",
74 | "ts-node": "^10.8.2",
75 | "tsconfig-paths": "^4.0.0",
76 | "typescript": "^4.7.4"
77 | },
78 | "jest": {
79 | "moduleFileExtensions": [
80 | "js",
81 | "json",
82 | "ts"
83 | ],
84 | "rootDir": "src",
85 | "moduleNameMapper": {
86 | "^src/(.*)$": "/$1"
87 | },
88 | "testRegex": ".*\\.spec\\.ts$",
89 | "transform": {
90 | "^.+\\.(t|j)s$": "ts-jest"
91 | },
92 | "collectCoverageFrom": [
93 | "**/*.{!(module|dto|entity|constants),}.(t|j)s"
94 | ],
95 | "coverageDirectory": "../coverage",
96 | "testEnvironment": "node"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 | import { ApiOperation, ApiTags } from '@nestjs/swagger';
4 |
5 | @Controller()
6 | @ApiTags('Default API')
7 | export class AppController {
8 | constructor(private readonly appService: AppService) {}
9 |
10 | @Get()
11 | @ApiOperation({
12 | summary: 'Health Check API',
13 | description: 'Health Check API',
14 | })
15 | getHello(): string {
16 | return this.appService.getHello();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { UsersModule } from './domain/users/users.module';
5 | import { ConfigModule } from '@nestjs/config';
6 | import { NoticeModule } from './domain/notice/notice.module';
7 | import { TypeOrmModule } from '@nestjs/typeorm';
8 | import { DataSource } from 'typeorm';
9 | import { Notice } from './domain/notice/entities/notice.entity';
10 | import { AuthModule } from './auth/auth.module';
11 | import { User } from './domain/users/entities/user.entity';
12 | import { UserInfo } from './domain/users/entities/userInfo.entity';
13 | import { Social } from './domain/users/entities/social.entity';
14 | import { Friend } from './domain/friend/entities/friend.entity';
15 | import { LetterModule } from './domain/letter/letter.module';
16 | import { ReceivedLetter } from './domain/letter/entities/receivedLetter.entity';
17 | import { ReminderModule } from './domain/reminder/reminder.module';
18 | import { Reminder } from './domain/reminder/entities/reminder.entity';
19 | import { LetterBody } from './domain/letter/entities/letterBody.entity';
20 | import { Reply } from './domain/reply/entities/reply.entity';
21 | import { Relationship } from './domain/relationship/entities/relationship.entity';
22 | import { SendLetter } from './domain/letter/entities/sendLetter.entity';
23 | import { SentenceModule } from './domain/sentence/sentence.module';
24 | import { Sentence } from './domain/sentence/entities/sentence.entity';
25 | import { ReplyModule } from './domain/reply/reply.module';
26 | import { LoggerMiddleware } from './common/middlewares/logger.middleware';
27 | import { RedisModule } from '@liaoliaots/nestjs-redis';
28 | import * as winston from 'winston';
29 | import {
30 | utilities as nestWinstonModuleUtilities,
31 | WinstonModule,
32 | } from 'nest-winston';
33 | import { FriendModule } from './domain/friend/friend.module';
34 | import winstonDaily from 'winston-daily-rotate-file';
35 | import { APP_FILTER } from '@nestjs/core';
36 | import { HttpExceptionFilter } from './common/exceptions/httpExceptionFilter';
37 | import { KakaoModule } from './domain/kakao/kakao.module';
38 |
39 | const ConfigSettingModule = ConfigModule.forRoot({
40 | isGlobal: true,
41 | });
42 |
43 | const TypeOrmSettingModule = TypeOrmModule.forRoot({
44 | type: 'mysql',
45 | host: process.env.MYSQL_HOST,
46 | port: parseInt(process.env.MYSQL_PORT, 10) || 3306,
47 | username: process.env.MYSQL_USER,
48 | password: process.env.MYSQL_PASSWORD,
49 | database: process.env.MYSQL_DB,
50 | timezone: 'Asia/Seoul',
51 |
52 | entities: [
53 | Notice,
54 | User,
55 | UserInfo,
56 | Social,
57 | Friend,
58 | ReceivedLetter,
59 | LetterBody,
60 | Reply,
61 | Relationship,
62 | SendLetter,
63 | Reminder,
64 | Sentence,
65 | ],
66 |
67 | synchronize: false,
68 | logging: 'all',
69 | });
70 |
71 | const RedisSettingModule = RedisModule.forRoot({
72 | config: {
73 | host: process.env.REDIS_HOST,
74 | port: parseInt(process.env.REDIS_PORT, 10) || 6379,
75 | },
76 | });
77 |
78 | const logDir = 'logs';
79 |
80 | const dailyLoggerOptions = (level: string) => {
81 | return {
82 | level,
83 | datePattern: 'YYYY-MM-DD',
84 | dirname: logDir + `/${level}`,
85 | filename: `${level}-%DATE%.log`,
86 | maxFiles: '14d',
87 | zippedArchive: true,
88 | };
89 | };
90 |
91 | const WinstonSettingModule = WinstonModule.forRoot({
92 | transports: [
93 | new winston.transports.Console({
94 | format: winston.format.combine(
95 | winston.format.timestamp(),
96 | nestWinstonModuleUtilities.format.nestLike('ggo-geet', {
97 | prettyPrint: true,
98 | }),
99 | ),
100 | }),
101 | new winstonDaily(dailyLoggerOptions('error')),
102 | new winstonDaily(dailyLoggerOptions('warn')),
103 | new winstonDaily(dailyLoggerOptions('info')),
104 | ],
105 | });
106 |
107 | @Module({
108 | imports: [
109 | ConfigSettingModule,
110 | TypeOrmSettingModule,
111 | RedisSettingModule,
112 | WinstonSettingModule,
113 | UsersModule,
114 | NoticeModule,
115 | AuthModule,
116 | LetterModule,
117 | SentenceModule,
118 | ReminderModule,
119 | ReplyModule,
120 | FriendModule,
121 | KakaoModule,
122 | ],
123 | controllers: [AppController],
124 | providers: [
125 | AppService,
126 | {
127 | provide: APP_FILTER,
128 | useClass: HttpExceptionFilter,
129 | },
130 | ],
131 | })
132 | export class AppModule implements NestModule {
133 | constructor(private dataSource: DataSource) {}
134 |
135 | configure(consumer: MiddlewareConsumer) {
136 | consumer.apply(LoggerMiddleware).forRoutes('*');
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Get,
5 | HttpStatus,
6 | Post,
7 | Query,
8 | Res,
9 | } from '@nestjs/common';
10 | import { AuthService } from './auth.service';
11 | import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
12 | import { ResponseFriendDto } from '../domain/friend/dto/response/responseFriend.dto';
13 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
14 | import { KakaoToken } from 'src/domain/kakao/kakaoToken';
15 | import { FriendService } from 'src/domain/friend/friend.service';
16 |
17 | @Controller('auth')
18 | @ApiTags('Auth API')
19 | export class AuthController {
20 | constructor(
21 | private readonly authService: AuthService,
22 | private readonly friendsService: FriendService,
23 | private readonly kakaoTokenRepository: KakaoTokenRepository,
24 | ) {}
25 |
26 | @ApiOperation({
27 | summary: '카카오 로그인 API',
28 | description:
29 | '카카오 로그인을 진행합니다. code는 로그인 엑세스토큰 요청을 위해 카카오에서 받은 엑세스코드입니다. callbackurl이 /login인지 확인 필요',
30 | })
31 | @Post('/login')
32 | @ApiBody({
33 | schema: {
34 | properties: {
35 | code: {
36 | type: 'string',
37 | example: 'code',
38 | },
39 | redirectURI: {
40 | type: 'string',
41 | example: 'https://ggo-geet.com/bla/bla',
42 | },
43 | },
44 | },
45 | })
46 | @ApiResponse({
47 | status: HttpStatus.OK || HttpStatus.CREATED,
48 | description: 'Jwt Access 토큰과 Refresh 토큰을 반환합니다.',
49 | schema: {
50 | properties: {
51 | accessToken: {
52 | type: 'string',
53 | example: 'accessToken',
54 | description: 'Jwt 토큰',
55 | },
56 | refreshToken: {
57 | type: 'string',
58 | example: 'refreshToken',
59 | description: 'Refresh 토큰',
60 | },
61 | allowFriendsList: {
62 | type: 'boolean',
63 | example: 'true',
64 | description: '친구목록,메세지 동의 여부',
65 | },
66 | },
67 | },
68 | })
69 | async kakaoLogin(
70 | @Body('code') code: string,
71 | @Body('redirectURI') redirectURI: string,
72 | @Res() res,
73 | ) {
74 | // 인증 코드로 카카오 토큰 받아오기
75 | const codeResponse = await this.authService.getKakaoAccessToken(
76 | code,
77 | redirectURI,
78 | );
79 |
80 | const kakaoToken = new KakaoToken(
81 | codeResponse.access_token,
82 | codeResponse.refresh_token,
83 | codeResponse.expires_in,
84 | codeResponse.refresh_token_expires_in,
85 | );
86 |
87 | // 토큰으로 사용자 정보 받아오기
88 | const { statusCode, user, allowFriendsList } =
89 | await this.authService.getUserProfile(codeResponse);
90 | await this.kakaoTokenRepository.save(user.id, kakaoToken);
91 |
92 | if (allowFriendsList == true) {
93 | await this.friendsService.updateFriends(user);
94 | }
95 |
96 | // user.id로 jwt 토큰 발급
97 | const jwtAccessToken = await this.authService.getAccessToken(user.id);
98 | const jwtRefreshToken = await this.authService.getRefreshToken(user.id);
99 |
100 | res
101 | .status(statusCode)
102 | .send({ data: { jwtAccessToken, jwtRefreshToken, allowFriendsList } });
103 | }
104 |
105 | @ApiOperation({
106 | summary: '카카오 친구목록 API',
107 | description:
108 | '카카오 친구목록 받기 위한 추가 항목 동의를 요청하고 친구목록을 반환합니다.',
109 | })
110 | @ApiBody({
111 | description:
112 | 'code는 친구목록 엑세스토큰 요청을 위해 카카오에서 받은 엑세스코드입니다. callbackurl이 /friends인지 확인 필요',
113 | schema: {
114 | properties: {
115 | code: {
116 | type: 'string',
117 | example: 'code',
118 | },
119 | redirectURI: {
120 | type: 'string',
121 | example: 'https://ggo-geet.com/bla/bla',
122 | },
123 | },
124 | },
125 | })
126 | @ApiResponse({
127 | status: HttpStatus.OK,
128 | description: '친구목록을 반환합니다.',
129 | type: [ResponseFriendDto],
130 | })
131 | // 추가 동의항목 동의 후 친구목록 반환
132 | @Post('/kakao/friends')
133 | async addAgreeCategory(
134 | @Body('code') code: string,
135 | @Body('redirectURI') redirectURI: string,
136 | @Res() res,
137 | ) {
138 | const codeResponse = await this.authService.getKakaoAccessToken(
139 | code,
140 | redirectURI,
141 | );
142 |
143 | const { user } = await this.authService.getUserProfile(codeResponse);
144 |
145 | const kakaoToken = new KakaoToken(
146 | codeResponse.access_token,
147 | codeResponse.refresh_token,
148 | codeResponse.expires_in,
149 | codeResponse.refresh_token_expires_in,
150 | );
151 |
152 | await this.kakaoTokenRepository.save(user.id, kakaoToken);
153 |
154 | await this.friendsService.updateFriends(user);
155 |
156 | res.status(HttpStatus.OK).send();
157 | }
158 |
159 | //-----------------------------------------------------------------------------------------------------------
160 | @ApiOperation({
161 | summary: '카카오 로그인 테스트용 API',
162 | description: '로그인할때 body에 넣을 code값을 받아옵니다.',
163 | })
164 | @Get('/code/login')
165 | async getCodeForLoginTest(@Res() res) {
166 | const _hostName = 'https://kauth.kakao.com';
167 | const _restApiKey = process.env.KAKAO_CLIENT_ID;
168 | // 카카오 로그인 redirectURI 등록
169 | const _redirectUrl = `${process.env.FRONT_HOST}/auth/kakao/login`;
170 | const url = `${_hostName}/oauth/authorize?client_id=${_restApiKey}&redirect_uri=${_redirectUrl}&response_type=code`;
171 | return res.redirect(url);
172 | }
173 |
174 | @ApiOperation({
175 | summary: '카카오 친구목록 테스트용 API',
176 | description: '친구목록을 가져올때 body에 넣을 code값을 받아옵니다.',
177 | })
178 | @Get('/code/friends')
179 | async getCodeForFriendsTest(@Res() res) {
180 | const _hostName = 'https://kauth.kakao.com';
181 | const _restApiKey = process.env.KAKAO_CLIENT_ID;
182 | // 카카오 로그인 redirectURI 등록
183 | const _redirectUrl = `${process.env.FRONT_HOST}/auth/kakao/friends`;
184 | const url = `${_hostName}/oauth/authorize?client_id=${_restApiKey}&redirect_uri=${_redirectUrl}&response_type=code&scope=friends,talk_message`;
185 | return res.redirect(url);
186 | }
187 |
188 | @ApiOperation({
189 | summary: '카카오 로그인 Redirect API',
190 | description: '카카오 로그인 Redirect API',
191 | })
192 | @Get('/kakao/login')
193 | async kakaoLoginTest(@Query('code') code: string, @Res() res) {
194 | return res.send({ scope: 'login', code });
195 | }
196 |
197 | @ApiOperation({
198 | summary: '카카오 친구목록 Redirect API',
199 | description: '카카오 친구목록 Redirect API',
200 | })
201 | @Get('/kakao/friends')
202 | async kakaoFriendsTest(@Query('code') code: string, @Res() res) {
203 | return res.send({ scope: 'friends', code });
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { JwtModule } from '@nestjs/jwt';
3 | import { PassportModule } from '@nestjs/passport';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
6 | import { Social } from 'src/domain/users/entities/social.entity';
7 | import { User } from 'src/domain/users/entities/user.entity';
8 | import { UserInfo } from 'src/domain/users/entities/userInfo.entity';
9 | import { AuthController } from './auth.controller';
10 | import { AuthService } from './auth.service';
11 | import { KakaoService } from 'src/domain/kakao/kakao.service';
12 | import { Friend } from 'src/domain/friend/entities/friend.entity';
13 | import { UsersService } from 'src/domain/users/users.service';
14 | import { FriendService } from 'src/domain/friend/friend.service';
15 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
16 |
17 | @Module({
18 | imports: [
19 | PassportModule,
20 | JwtModule.register({
21 | secret: process.env.JWT_SECRET,
22 | signOptions: {
23 | expiresIn: process.env.JWT_ACCESS_EXPIRATION_TIME,
24 | },
25 | }),
26 | TypeOrmModule.forFeature([User, UserInfo, Social, Friend]),
27 | ],
28 | controllers: [AuthController],
29 | providers: [
30 | AuthService,
31 | JwtStrategy,
32 | KakaoService,
33 | UsersService,
34 | FriendService,
35 | KakaoTokenRepository,
36 | ],
37 | exports: [AuthService, JwtModule, PassportModule, JwtStrategy],
38 | })
39 | export class AuthModule {}
40 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, InternalServerErrorException } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { InjectRepository } from '@nestjs/typeorm';
4 | import { Social } from 'src/domain/users/entities/social.entity';
5 | import { User } from 'src/domain/users/entities/user.entity';
6 | import { UserInfo } from 'src/domain/users/entities/userInfo.entity';
7 | import { Repository } from 'typeorm';
8 | import { CreateKakaoUserDto } from './dto/requests/createKakaoUser.dto';
9 | import { KakaoService } from 'src/domain/kakao/kakao.service';
10 | import { UsersService } from 'src/domain/users/users.service';
11 |
12 | @Injectable()
13 | export class AuthService {
14 | constructor(
15 | @InjectRepository(User) private userRepository: Repository,
16 | private readonly userService: UsersService,
17 | private readonly kakaoService: KakaoService,
18 | private readonly jwtService: JwtService,
19 | ) {}
20 |
21 | async getKakaoAccessToken(code: string, redirectURI: string) {
22 | return await this.kakaoService.getKakaoAccessToken(code, redirectURI);
23 | }
24 |
25 | async getUserProfile(codeResponse) {
26 | try {
27 | const profileJson = await this.kakaoService.getKakaoProfile(
28 | codeResponse.access_token,
29 | );
30 | const kakao_account = profileJson.kakao_account;
31 |
32 | const kakaoInfo: CreateKakaoUserDto = {
33 | kakaoId: profileJson.id,
34 | nickname: profileJson.properties.nickname,
35 | profile_image: profileJson.properties.profile_image,
36 | email:
37 | kakao_account.has_email && !kakao_account.kakao_email_needs_agreement
38 | ? kakao_account.email
39 | : null,
40 | age_range:
41 | kakao_account.has_age_range &&
42 | !kakao_account.kakao_age_range_needs_agreement
43 | ? kakao_account.age_range
44 | : null,
45 | birthday:
46 | kakao_account.has_birthday &&
47 | !kakao_account.kakao_birthday_needs_agreement
48 | ? kakao_account.birthday
49 | : null,
50 | gender:
51 | kakao_account.has_gender &&
52 | !kakao_account.kakao_gender_needs_agreement
53 | ? kakao_account.gender
54 | : null,
55 | allow_scope: codeResponse.scope,
56 | is_allow_friend: codeResponse.scope.indexOf('friends') !== -1,
57 | };
58 |
59 | // 존재하는 유저인지 확인 후 유저 정보 반환
60 | const user = await this.validateKakao(kakaoInfo);
61 | return user;
62 | } catch (e) {
63 | throw new InternalServerErrorException(e);
64 | }
65 | }
66 |
67 | async validateKakao(kakaoInfo: CreateKakaoUserDto) {
68 | const kakaoId: number = parseInt(kakaoInfo.kakaoId);
69 | let exUser = await this.userService.findUserBySocialId(kakaoId);
70 |
71 | // 새로 가입한 유저면 create
72 | if (!exUser) {
73 | const newSocial = new Social();
74 | newSocial.clientId = kakaoInfo.kakaoId;
75 | newSocial.allowFriendsList = kakaoInfo.is_allow_friend;
76 |
77 | const newUserInfo = new UserInfo();
78 | newUserInfo.email = kakaoInfo.email;
79 | newUserInfo.birthday = kakaoInfo.birthday;
80 | newUserInfo.email = kakaoInfo.email;
81 | newUserInfo.gender = kakaoInfo.gender;
82 |
83 | const newUser = new User();
84 | newUser.name = kakaoInfo.nickname;
85 | newUser.nickname = kakaoInfo.nickname;
86 | newUser.profileImg = kakaoInfo.profile_image;
87 | newUser.social = newSocial;
88 | newUser.userInfo = newUserInfo;
89 |
90 | await this.userRepository.save(newUser);
91 | return {
92 | statusCode: 201,
93 | user: newUser,
94 | allowFriendsList: newUser.social.allowFriendsList,
95 | };
96 | } else {
97 | // 기존 유저면 update
98 | exUser = await this.updateKakaoUser(exUser.id, kakaoInfo);
99 | return {
100 | statusCode: 200,
101 | user: exUser,
102 | allowFriendsList: exUser.social.allowFriendsList,
103 | };
104 | }
105 | }
106 |
107 | async getAccessToken(userId: number) {
108 | return this.jwtService.sign(
109 | { id: userId },
110 | {
111 | secret: process.env.JWT_SECRET,
112 | expiresIn: process.env.JWT_ACCESS_EXPIRATION_TIME,
113 | },
114 | );
115 | }
116 |
117 | async getRefreshToken(userId: number) {
118 | return this.jwtService.sign(
119 | { id: userId },
120 | {
121 | secret: process.env.JWT_SECRET,
122 | expiresIn: process.env.JWT_REFRESH_EXPIRATION_TIME,
123 | },
124 | );
125 | }
126 |
127 | async updateKakaoUser(id: number, kakaoInfo: CreateKakaoUserDto) {
128 | const user = await this.userRepository.findOne({
129 | where: { id: id },
130 | relations: { social: true },
131 | });
132 | user.name = kakaoInfo.nickname;
133 | user.profileImg = kakaoInfo.profile_image;
134 | user.social.allowFriendsList = kakaoInfo.is_allow_friend;
135 |
136 | return await this.userRepository.save(user);
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/auth/dto/requests/createKakaoUser.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateKakaoUserDto {
2 | kakaoId: string;
3 | nickname: string;
4 | profile_image: string;
5 | email: string;
6 | age_range: string;
7 | birthday: string;
8 | gender: string;
9 | allow_scope: string;
10 | is_allow_friend: boolean;
11 | }
12 |
--------------------------------------------------------------------------------
/src/auth/dto/requests/updateKakaoUser.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 | import { CreateKakaoUserDto } from './createKakaoUser.dto';
3 |
4 | export class UpdateKakaoUserDto extends PartialType(CreateKakaoUserDto) {}
5 |
--------------------------------------------------------------------------------
/src/auth/strategy/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, UnauthorizedException } from '@nestjs/common';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { InjectRepository } from '@nestjs/typeorm';
4 | import { ExtractJwt, Strategy } from 'passport-jwt';
5 | import { User } from 'src/domain/users/entities/user.entity';
6 | import { Repository } from 'typeorm';
7 |
8 | @Injectable()
9 | export class JwtStrategy extends PassportStrategy(Strategy) {
10 | constructor(
11 | @InjectRepository(User) private userRepository: Repository,
12 | ) {
13 | super({
14 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
15 | ignoreExpiration: false,
16 | secretOrKey: process.env.JWT_SECRET,
17 | });
18 | }
19 |
20 | async validate(payload) {
21 | const user = await this.userRepository.findOneBy({ id: payload.id });
22 |
23 | if (user) {
24 | return user;
25 | } else {
26 | throw new UnauthorizedException({
27 | type: 'UNAUTHORIZED',
28 | message: 'Authentication error.',
29 | });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/auth/test/auth.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthController } from '../auth.controller';
2 |
3 | describe('AuthController', () => {
4 | let controller: AuthController;
5 |
6 | it('should be defined', () => {
7 | expect(true).toBe(true);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/auth/test/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from '../auth.service';
2 |
3 | describe('AuthService', () => {
4 | let service: AuthService;
5 |
6 | it('should be defined', () => {
7 | expect(true).toBe(true);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/common/decorators/pagination.decorators.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 | import {
3 | PaginationDefault,
4 | PaginationRequest,
5 | } from '../paginations/pagination.request';
6 |
7 | /*
8 | * @GetPagination() is a decorator that returns a PagenationRequest object.
9 | * @GetPagination() pagination: PaginationRequest
10 | */
11 | export const GetPagination = createParamDecorator(
12 | (_data: any, ctx: ExecutionContext) => {
13 | const request = ctx.switchToHttp().getRequest();
14 | const page = request.query?.page || PaginationDefault.PAGE_DEFAULT;
15 | const take = request.query?.take || PaginationDefault.TAKE_DEFAULT;
16 |
17 | const pagenationRequest: PaginationRequest = {
18 | page: page,
19 | take: take,
20 | getPage: () => {
21 | return page;
22 | },
23 | getSkip: () => {
24 | return (page - 1) * take;
25 | },
26 | getTake: () => {
27 | return take;
28 | },
29 | };
30 |
31 | return pagenationRequest;
32 | },
33 | );
34 |
--------------------------------------------------------------------------------
/src/common/decorators/user.decorators.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 |
3 | export const ReqUser = createParamDecorator(
4 | (_data: any, ctx: ExecutionContext) => {
5 | const request = ctx.switchToHttp().getRequest();
6 | const userObj = request.user;
7 |
8 | return userObj;
9 | },
10 | );
11 |
--------------------------------------------------------------------------------
/src/common/exceptions/httpExceptionFilter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | HttpException,
6 | Inject,
7 | } from '@nestjs/common';
8 | import { Request, Response } from 'express';
9 | import { WINSTON_MODULE_PROVIDER, WinstonLogger } from 'nest-winston';
10 | import { TelegramMonitoringService } from 'src/infrastructor/monitoring/monitoring.telegram';
11 |
12 | @Catch()
13 | export class HttpExceptionFilter implements ExceptionFilter {
14 | constructor(
15 | @Inject(WINSTON_MODULE_PROVIDER)
16 | private readonly logger: WinstonLogger,
17 | ) {}
18 | catch(exception: Error, host: ArgumentsHost) {
19 | const ctx = host.switchToHttp();
20 | const res = ctx.getResponse();
21 | const req = ctx.getRequest();
22 | const errorTime = new Date().toLocaleString();
23 | const monitoring = new TelegramMonitoringService();
24 |
25 | if (!(exception instanceof HttpException) || res.statusCode >= 500) {
26 | const errorMsg = `[${req.method}] ${req.url} ${res.statusCode} - ${exception} - ${errorTime}`;
27 | monitoring.sendAlert(errorMsg);
28 | this.logger.error(errorMsg);
29 |
30 | return res.status(500).json({
31 | statusCode: 500,
32 | message: 'Internal Server Error',
33 | error: 'Internal Server Error',
34 | });
35 | }
36 |
37 | const response = (exception as HttpException).getResponse();
38 | const status = (exception as HttpException).getStatus();
39 |
40 | const httpErrorMsg = `[${req.method}] ${req.url} ${status} - ${exception} - ${errorTime}`;
41 | this.logger.error(httpErrorMsg);
42 |
43 | res.status((exception as HttpException).getStatus()).json(response);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/common/guards/jwtAuth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class JwtAuthGuard extends AuthGuard('jwt') {}
6 |
--------------------------------------------------------------------------------
/src/common/middlewares/logger.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Request, Response } from 'express';
3 | import { Logger as WinstonLogger } from 'winston';
4 | import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
5 |
6 | @Injectable()
7 | export class LoggerMiddleware implements NestMiddleware {
8 | constructor(
9 | @Inject(WINSTON_MODULE_PROVIDER)
10 | private readonly logger: WinstonLogger,
11 | ) {}
12 |
13 | use(req: Request, res: Response, next: NextFunction) {
14 | const { ip, method, originalUrl } = req;
15 | const userAgent = req.get('user-agent') || '';
16 |
17 | res.on('finish', () => {
18 | const { statusCode } = res;
19 | this.logger.info(
20 | `[${method}] ${originalUrl} ${statusCode} - ${ip} ${userAgent}`,
21 | );
22 | });
23 |
24 | next();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/common/paginations/pagination.request.ts:
--------------------------------------------------------------------------------
1 | import { Type } from 'class-transformer';
2 | import { IsOptional } from 'class-validator';
3 |
4 | export enum PaginationDefault {
5 | PAGE_DEFAULT = 1,
6 | TAKE_DEFAULT = 10,
7 | SKIP_DEFAULT = 0,
8 | }
9 |
10 | export class PaginationRequest {
11 | @Type(() => Number)
12 | @IsOptional()
13 | page?: number = PaginationDefault.PAGE_DEFAULT;
14 |
15 | @Type(() => Number)
16 | @IsOptional()
17 | take?: number = PaginationDefault.TAKE_DEFAULT;
18 |
19 | getSkip() {
20 | return (this.page - 1) * this.take || PaginationDefault.SKIP_DEFAULT;
21 | }
22 |
23 | getPage() {
24 | return this.page;
25 | }
26 |
27 | getTake() {
28 | return this.take;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/common/paginations/pagination.response.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { PaginationBuilder } from './paginationBuilder.response';
3 |
4 | export class PaginationResponse {
5 | data: T[];
6 |
7 | @ApiProperty({
8 | example: {
9 | page: 1,
10 | take: 10,
11 | totalCount: 100,
12 | totalPage: 10,
13 | hasNextPage: true,
14 | },
15 | description: '페이지네이션관련 메타데이터',
16 | })
17 | meta: {
18 | page: number;
19 | take: number;
20 | totalCount: number;
21 | totalPage: number;
22 | hasNextPage: boolean;
23 | };
24 |
25 | constructor(pagenationBuilder: PaginationBuilder) {
26 | this.data = pagenationBuilder._data;
27 | this.meta = {
28 | page: pagenationBuilder._page,
29 | take: pagenationBuilder._take,
30 | totalCount: pagenationBuilder._totalCount,
31 | totalPage: this.getTotalPage(
32 | pagenationBuilder._totalCount,
33 | pagenationBuilder._take,
34 | ),
35 | hasNextPage: this.getHasNextPage(
36 | pagenationBuilder._page,
37 | this.getTotalPage(
38 | pagenationBuilder._totalCount,
39 | pagenationBuilder._take,
40 | ),
41 | ),
42 | };
43 | }
44 |
45 | private getTotalPage(totalCount: number, take: number): number {
46 | return Math.ceil(totalCount / take);
47 | }
48 |
49 | private getHasNextPage(page: number, totalPage: number): boolean {
50 | return page < totalPage;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/common/paginations/pagination.swagger.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators, Type } from '@nestjs/common';
2 | import {
3 | ApiExtraModels,
4 | ApiOkResponse,
5 | ApiQuery,
6 | getSchemaPath,
7 | } from '@nestjs/swagger';
8 | import { PaginationResponse } from './pagination.response';
9 |
10 | /*
11 | * @ApiPaginatedResponse() is a decorator to create a swagger document for a paginated response.
12 | */
13 | export const ApiPaginationResponse = (model: TModel) => {
14 | return applyDecorators(
15 | ApiOkResponse({
16 | schema: {
17 | allOf: [
18 | { $ref: getSchemaPath(PaginationResponse) },
19 | {
20 | properties: {
21 | data: {
22 | type: 'array',
23 | items: { $ref: getSchemaPath(model) },
24 | },
25 | },
26 | },
27 | ],
28 | },
29 | }),
30 | ApiExtraModels(PaginationResponse),
31 | ApiExtraModels(model),
32 | );
33 | };
34 |
35 | /*
36 | * @ApiPaginationRequst() is a decorator to create a swagger document for a paginated request.
37 | */
38 | export const ApiPaginationRequst = () => {
39 | return applyDecorators(
40 | ApiQuery({
41 | name: 'page',
42 | required: false,
43 | type: Number,
44 | description: '페이지 번호',
45 | }),
46 | ApiQuery({
47 | name: 'take',
48 | required: false,
49 | type: Number,
50 | description: '페이지당 아이템 수',
51 | }),
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/common/paginations/paginationBuilder.response.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * PaginationBuilder is a class to create a new PageResponse object
3 | * Using Builder Pattern, To prevent the error of the order of the arguments
4 | * when creating a new PageResponse object
5 | * .setData(data)
6 | * .setPage(page)
7 | * .setTake(take)
8 | * .setTotalCount(totalCount)
9 | * .build()
10 | */
11 |
12 | import { PaginationResponse } from './pagination.response';
13 |
14 | export class PaginationBuilder {
15 | _data: T[];
16 | _page: number;
17 | _take: number;
18 | _totalCount: number;
19 |
20 | setData(data: T[]) {
21 | this._data = data;
22 | return this;
23 | }
24 |
25 | setPage(page: number) {
26 | this._page = page;
27 | return this;
28 | }
29 |
30 | setTake(take: number) {
31 | this._take = take;
32 | return this;
33 | }
34 |
35 | setTotalCount(totalCount: number) {
36 | this._totalCount = totalCount;
37 | return this;
38 | }
39 |
40 | build() {
41 | return new PaginationResponse(this);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/domain/friend/dto/response/responseFriend.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Friend } from 'src/domain/friend/entities/friend.entity';
3 |
4 | export class ResponseFriendDto {
5 | @ApiProperty({ example: 1, description: '유저 id' })
6 | id: number;
7 | @ApiProperty({ example: 1, description: '친구 user id' })
8 | friendUserId: number;
9 |
10 | @ApiProperty({ example: 'kakao-uuid', description: '카카오 uuid' })
11 | kakaoUuid: string;
12 |
13 | @ApiProperty({ example: '친구 이름', description: '친구 이름' })
14 | kakaoFriendName: string;
15 |
16 | @ApiProperty({
17 | example: '친구 프로필 이미지',
18 | description: '친구 프로필 이미지',
19 | })
20 | friendProfileImg: string;
21 |
22 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '생성일' })
23 | createdAt: Date;
24 |
25 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '수정일' })
26 | updatedAt: Date;
27 |
28 | constructor(friend: Friend) {
29 | this.id = friend.id;
30 | this.friendUserId = friend.friendUser.id;
31 | this.friendProfileImg = friend.friendUser.profileImg;
32 | this.kakaoUuid = friend.kakaoUuid;
33 | this.kakaoFriendName = friend.kakaoFriendName;
34 | this.createdAt = friend.createdAt;
35 | this.updatedAt = friend.updatedAt;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/domain/friend/entities/friend.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | DeleteDateColumn,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | PrimaryGeneratedColumn,
9 | UpdateDateColumn,
10 | } from 'typeorm';
11 | import { User } from '../../users/entities/user.entity';
12 |
13 | @Entity({ name: 'friend' })
14 | export class Friend {
15 | @PrimaryGeneratedColumn()
16 | id: number;
17 |
18 | @Column()
19 | kakaoUuid: string;
20 |
21 | @Column()
22 | kakaoFriendName: string;
23 |
24 | @ManyToOne(() => User)
25 | @JoinColumn({ name: 'userId' })
26 | user: User;
27 |
28 | @Column({ name: 'userId' })
29 | userId: number;
30 |
31 | @ManyToOne(() => User, { nullable: true })
32 | @JoinColumn({ name: 'friendUserId' })
33 | friendUser: User;
34 |
35 | @CreateDateColumn()
36 | createdAt: Date;
37 |
38 | @UpdateDateColumn()
39 | updatedAt: Date;
40 |
41 | @DeleteDateColumn()
42 | deletedAt: Date;
43 | }
44 |
--------------------------------------------------------------------------------
/src/domain/friend/friend.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | HttpStatus,
5 | Param,
6 | Patch,
7 | UseGuards,
8 | } from '@nestjs/common';
9 | import { FriendService } from './friend.service';
10 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
11 | import { ReqUser } from 'src/common/decorators/user.decorators';
12 | import { User } from 'src/domain/users/entities/user.entity';
13 | import {
14 | ApiBearerAuth,
15 | ApiOperation,
16 | ApiResponse,
17 | ApiTags,
18 | } from '@nestjs/swagger';
19 | import { ResponseFriendDto } from './dto/response/responseFriend.dto';
20 |
21 | @Controller('friends')
22 | @ApiBearerAuth()
23 | @ApiTags('Friends API')
24 | @UseGuards(JwtAuthGuard)
25 | export class FriendController {
26 | constructor(private readonly friendService: FriendService) {}
27 |
28 | @ApiOperation({
29 | summary: '카카오 친구목록 가져오기 API',
30 | description: '카카오 친구목록을 가져옵니다.',
31 | })
32 | @ApiResponse({
33 | status: HttpStatus.OK,
34 | description: '친구목록을 반환합니다.',
35 | type: [ResponseFriendDto],
36 | })
37 | @Get()
38 | findAll(@ReqUser() user: User) {
39 | return this.friendService.findAll(user);
40 | }
41 |
42 | @ApiOperation({
43 | summary: '친구 정보 가져오기 API',
44 | description: '친구 정보를 친구 ID로 가져옵니다.',
45 | })
46 | @ApiResponse({
47 | status: HttpStatus.OK,
48 | description: '친구 정보를 반환합니다.',
49 | type: ResponseFriendDto,
50 | })
51 | @Get('/:id')
52 | findOne(@ReqUser() user: User, @Param('id') id: number) {
53 | return this.friendService.findOne(id, user);
54 | }
55 |
56 | @ApiOperation({
57 | summary: '카카오 친구 목록 업데이트 API',
58 | description: '친구 목록을 카카오 친구 기반으로 업데이트합니다.',
59 | })
60 | @Patch('/kakao/update')
61 | updateFriends(@ReqUser() user: User) {
62 | return this.friendService.updateFriends(user);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/domain/friend/friend.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { FriendController } from './friend.controller';
3 | import { FriendService } from './friend.service';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { PassportModule } from '@nestjs/passport';
6 | import { AuthModule } from 'src/auth/auth.module';
7 | import { Friend } from './entities/friend.entity';
8 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
9 | import { User } from 'src/domain/users/entities/user.entity';
10 | import { KakaoService } from 'src/domain/kakao/kakao.service';
11 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
12 |
13 | @Module({
14 | imports: [
15 | TypeOrmModule.forFeature([User, Friend]),
16 | PassportModule,
17 | AuthModule,
18 | ],
19 | controllers: [FriendController],
20 | providers: [FriendService, JwtStrategy, KakaoService, KakaoTokenRepository],
21 | })
22 | export class FriendModule {}
23 |
--------------------------------------------------------------------------------
/src/domain/friend/friend.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { ResponseFriendDto } from './dto/response/responseFriend.dto';
4 | import { User } from 'src/domain/users/entities/user.entity';
5 | import { Friend } from './entities/friend.entity';
6 | import { Repository } from 'typeorm';
7 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
8 | import { KakaoToken } from 'src/domain/kakao/kakaoToken';
9 | import { KakaoService } from 'src/domain/kakao/kakao.service';
10 |
11 | @Injectable()
12 | export class FriendService {
13 | constructor(
14 | @InjectRepository(Friend)
15 | private readonly friendRepository: Repository,
16 | @InjectRepository(User)
17 | private readonly userRepository: Repository,
18 | private readonly kakaoService: KakaoService,
19 | private readonly kakaoTokenRepository: KakaoTokenRepository,
20 | ) {}
21 |
22 | async findAll(user: User): Promise {
23 | const friendList = await this.friendRepository
24 | .createQueryBuilder('friend')
25 | .leftJoinAndSelect('friend.friendUser', 'user')
26 | .where('friend.userId = :userId', { userId: user.id })
27 | .getMany();
28 | console.log(friendList);
29 | const result = friendList.map((friend) => {
30 | return new ResponseFriendDto(friend);
31 | });
32 | return {
33 | data: result,
34 | };
35 | }
36 |
37 | async findOne(id: number, user: User): Promise {
38 | const friend = await this.friendRepository
39 | .createQueryBuilder('friend')
40 | .leftJoinAndSelect('friend.friendUser', 'user')
41 | .where('friend.id = :id', { id: id })
42 | .andWhere('friend.userId = :userId', { userId: user.id })
43 | .getOne();
44 |
45 | if (!friend) {
46 | throw new NotFoundException({
47 | type: 'NOT_FOUND',
48 | message: `Friend #${id} not found`,
49 | });
50 | }
51 |
52 | return new ResponseFriendDto(friend);
53 | }
54 |
55 | async updateFriends(user: User) {
56 | const kakaoToken: KakaoToken = await this.kakaoTokenRepository.findByUserId(
57 | user.id,
58 | );
59 | const acessToken = kakaoToken.getAcessToken();
60 |
61 | if (!acessToken) {
62 | throw new NotFoundException({
63 | type: 'NOT_FOUND',
64 | message: `카카오 토큰이 없습니다.`,
65 | });
66 | }
67 |
68 | const [friendsList, friendsCount] =
69 | await this.kakaoService.getKakaoFriendsandCount(acessToken);
70 | if (friendsCount <= 0) {
71 | throw new NotFoundException({
72 | type: 'NOT_FOUND',
73 | message: `카카오 친구가 없습니다.`,
74 | });
75 | }
76 |
77 | class FriendsInfo {
78 | kakaoUuid: string;
79 | kakaoFriendName: string;
80 |
81 | constructor(kakaoUuid: string, kakaoFriendName: string) {
82 | this.kakaoUuid = kakaoUuid;
83 | this.kakaoFriendName = kakaoFriendName;
84 | }
85 |
86 | equals(friend: FriendsInfo) {
87 | return (
88 | this.kakaoUuid === friend.kakaoUuid &&
89 | this.kakaoFriendName === friend.kakaoFriendName
90 | );
91 | }
92 | }
93 |
94 | const exFriend = await this.friendRepository.find({
95 | where: { userId: user.id },
96 | select: ['kakaoUuid', 'kakaoFriendName'],
97 | });
98 |
99 | const exFriendList = exFriend.map((friend) => {
100 | return new FriendsInfo(friend.kakaoUuid, friend.kakaoFriendName);
101 | });
102 |
103 | const newFriendUuidList = friendsList.filter((friend) => {
104 | const f = new FriendsInfo(friend.uuid, friend.profile_nickname);
105 | return !exFriendList.some((t) => f.equals(t));
106 | });
107 |
108 | for await (const element of newFriendUuidList) {
109 | const exFriend = await this.friendRepository.findOne({
110 | where: { userId: user.id, kakaoUuid: element.uuid },
111 | });
112 | if (!exFriend) {
113 | await this.createKakaoFriends(element, user);
114 | } else if (exFriend.kakaoFriendName != element.profile_nickname) {
115 | exFriend.kakaoFriendName = element.profile_nickname;
116 | await this.friendRepository.save(exFriend);
117 | }
118 | }
119 | }
120 |
121 | async createKakaoFriends(element, user: User) {
122 | const friendUserId = await this.findUserByClientId(element.id);
123 | if (friendUserId == null) {
124 | return;
125 | }
126 | const friend = new Friend();
127 | friend.kakaoUuid = element.uuid;
128 | friend.kakaoFriendName = element.profile_nickname;
129 | friend.friendUser = friendUserId;
130 | friend.user = user;
131 |
132 | return await this.friendRepository.save(friend);
133 | }
134 |
135 | async findUserByClientId(clientId: string): Promise {
136 | const socialUser = await this.userRepository
137 | .createQueryBuilder('user')
138 | .leftJoinAndSelect('user.social', 'social')
139 | .where('social.clientId = :clientId', { clientId: clientId })
140 | .getOne();
141 | console.log(socialUser);
142 |
143 | return socialUser;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/domain/kakao/kakao.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { KakaoTokenRepository } from './repository/kakaoToken.memory.repository';
3 | import { RedisModule } from '@liaoliaots/nestjs-redis';
4 | import { KakaoService } from './kakao.service';
5 |
6 | @Module({
7 | imports: [RedisModule],
8 | controllers: [],
9 | providers: [KakaoTokenRepository, KakaoService],
10 | })
11 | export class KakaoModule {}
12 |
--------------------------------------------------------------------------------
/src/domain/kakao/kakao.service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import {
3 | InternalServerErrorException,
4 | NotFoundException,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import axios from 'axios';
8 | import qs from 'qs';
9 |
10 | export class KakaoService {
11 | authHostUrl = 'https://kauth.kakao.com';
12 | hostUrl = 'https://kapi.kakao.com';
13 |
14 | defaultHeaders = {
15 | 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
16 | };
17 |
18 | constructor() {}
19 |
20 | private addAuthHeader(access_token: string) {
21 | return {
22 | ...this.defaultHeaders,
23 | Authorization: `Bearer ${access_token}`,
24 | };
25 | }
26 |
27 | /*
28 | * Receive access tokens with Kakao login authentication code
29 | * @param code
30 | * @param redirect_uri
31 | * @returns {Promise}
32 | */
33 | async getKakaoAccessToken(code: string, redirect_uri: string): Promise {
34 | const authEndpoint = '/oauth/token';
35 | const url = `${this.authHostUrl}${authEndpoint}`;
36 |
37 | const body = {
38 | grant_type: 'authorization_code',
39 | client_id: process.env.KAKAO_CLIENT_ID,
40 | redirect_uri: redirect_uri,
41 | code: code,
42 | };
43 |
44 | try {
45 | const response = await axios({
46 | method: 'post',
47 | url: url,
48 | headers: this.defaultHeaders,
49 | data: qs.stringify(body),
50 | });
51 |
52 | if (response.status == 500) {
53 | throw new InternalServerErrorException({
54 | message: 'Kakao Internal Server Error',
55 | error: 'Kakao Internal Server Issue Cannot Get Friends',
56 | status: 500,
57 | });
58 | }
59 |
60 | return response.data;
61 | } catch (e) {
62 | throw new UnauthorizedException({
63 | message: 'Wrong kakaoAccessCode',
64 | error: "Can't get kakao access token",
65 | statusCode: 401,
66 | });
67 | }
68 | }
69 |
70 | async refreshKakaoAccessToken(refresh_token: string): Promise {
71 | const authEndpoint = '/oauth/token';
72 | const url = `${this.authHostUrl}${authEndpoint}`;
73 |
74 | const body = {
75 | grant_type: 'refresh_token',
76 | client_id: process.env.KAKAO_CLIENT_ID,
77 | refresh_token: refresh_token,
78 | };
79 |
80 | try {
81 | const response = await axios({
82 | method: 'post',
83 | url: url,
84 | headers: this.defaultHeaders,
85 | data: qs.stringify(body),
86 | });
87 |
88 | if (response.status == 500) {
89 | throw new InternalServerErrorException({
90 | message: 'Kakao Internal Server Error',
91 | error: 'Kakao Internal Server Issue Cannot Get Friends',
92 | status: 500,
93 | });
94 | }
95 |
96 | const { access_token, token_type, expires_in } = response.data;
97 |
98 | return {
99 | access_token,
100 | token_type,
101 | expires_in,
102 | };
103 | } catch (e) {
104 | throw new UnauthorizedException({
105 | message: 'Wrong kakaoAccessCode',
106 | error: "Can't refresh kakao access token",
107 | statusCode: 401,
108 | });
109 | }
110 | }
111 | /*
112 | * Get Kakao Friends and Count
113 | * @param access_token
114 | * @returns {Promise}
115 | */
116 | async getKakaoFriendsandCount(access_token: string): Promise {
117 | const friendsEndpoint = '/v1/api/talk/friends';
118 | const url = `${this.hostUrl}${friendsEndpoint}`;
119 |
120 | try {
121 | const response = await axios({
122 | method: 'get',
123 | url: url,
124 | headers: this.addAuthHeader(access_token),
125 | });
126 |
127 | if (response.status == 500) {
128 | throw new InternalServerErrorException({
129 | message: 'Kakao Internal Server Error',
130 | error: 'Kakao Internal Server Issue Cannot Get Friends',
131 | status: 500,
132 | });
133 | }
134 |
135 | if (response.status == 404) {
136 | return [[], 0];
137 | }
138 |
139 | return [response.data.elements, response.data.total_count];
140 | } catch (e) {
141 | throw new UnauthorizedException({
142 | message: 'Wrong kakaoAccessCode',
143 | error: "Can't get kakao friends",
144 | statusCode: 401,
145 | });
146 | }
147 | }
148 |
149 | /*
150 | * Get User Kakao Profile
151 | * @param access_token
152 | * @returns {Promise}
153 | * */
154 | async getKakaoProfile(access_token: string): Promise {
155 | const profileEndpoint = '/v2/user/me';
156 | const url = `${this.hostUrl}${profileEndpoint}`;
157 |
158 | try {
159 | const response = await axios({
160 | method: 'get',
161 | url: url,
162 | headers: this.addAuthHeader(access_token),
163 | });
164 |
165 | if (response.status == 500) {
166 | throw new InternalServerErrorException({
167 | message: 'Kakao Internal Server Error',
168 | error: 'Kakao Internal Server Issue Cannot Get Friends',
169 | status: 500,
170 | });
171 | }
172 |
173 | if (response.status == 404) {
174 | throw new UnauthorizedException({
175 | message: 'Not Registered User',
176 | error: 'Cannot Get Kakao Profile',
177 | status: 404,
178 | });
179 | }
180 |
181 | return response.data;
182 | } catch (e) {
183 | throw new UnauthorizedException(e, 'Wrong kakaoAccessCode');
184 | }
185 | }
186 |
187 | /*
188 | * Send Kakao Message
189 | * @param access_token
190 | * @param template_id
191 | * @param template_args
192 | * @returns {Promise}
193 | * */
194 | async sendKakaoMessage(
195 | access_token: string,
196 | kakao_uuid: string,
197 | template_id: number,
198 | template_args: any,
199 | ): Promise {
200 | const messageEndpoint = '/v1/api/talk/friends/message/send';
201 | const url = `${this.hostUrl}${messageEndpoint}`;
202 |
203 | const body = {
204 | template_id: template_id,
205 | receiver_uuids: '[' + '"' + kakao_uuid + '"' + ']',
206 | template_args: template_args,
207 | };
208 |
209 | try {
210 | const response = await axios({
211 | method: 'post',
212 | url: url,
213 | headers: this.addAuthHeader(access_token),
214 | data: qs.stringify(body),
215 | });
216 |
217 | if (response.status == 500) {
218 | throw new InternalServerErrorException({
219 | message: 'Kakao Internal Server Error',
220 | error: 'Kakao Internal Server Issue Cannot Get Friends',
221 | status: 500,
222 | });
223 | }
224 |
225 | if (response.status == 404) {
226 | throw new NotFoundException({
227 | message: 'Kakao Friend is not found',
228 | error: 'Cannot Send Kakao Message',
229 | status: 404,
230 | });
231 | }
232 |
233 | return response.data;
234 | } catch (e) {
235 | throw new UnauthorizedException({
236 | message: 'Wrong kakaoAccessCode',
237 | error: "Can't get kakao friends",
238 | statusCode: 401,
239 | });
240 | }
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/domain/kakao/kakaoToken.ts:
--------------------------------------------------------------------------------
1 | export class KakaoToken {
2 | private readonly accessToken: string;
3 | private readonly refreshToken: string;
4 | private readonly accessTokenExpiresIn: number;
5 | private readonly refreshTokenExpiresIn: number;
6 |
7 | constructor(
8 | accessToken: string,
9 | refreshToken: string,
10 | accessTokenExpiresIn: number,
11 | refreshTokenExpiresIn: number,
12 | ) {
13 | this.accessToken = accessToken;
14 | this.refreshToken = refreshToken;
15 | this.accessTokenExpiresIn = accessTokenExpiresIn;
16 | this.refreshTokenExpiresIn = refreshTokenExpiresIn;
17 | }
18 |
19 | getAcessToken(): string {
20 | return this.accessToken;
21 | }
22 |
23 | getExpiresIn(): number {
24 | return this.accessTokenExpiresIn;
25 | }
26 |
27 | getRefreshToken(): string {
28 | return this.refreshToken;
29 | }
30 |
31 | getRefreshTokenExpiresIn(): number {
32 | return this.refreshTokenExpiresIn;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/domain/kakao/repository/kakaoToken.memory.repository.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_REDIS_NAMESPACE, InjectRedis } from '@liaoliaots/nestjs-redis';
2 | import { KakaoToken } from '../kakaoToken';
3 | import Redis from 'ioredis';
4 | import { KakaoService } from '../kakao.service';
5 | import { InternalServerErrorException } from '@nestjs/common';
6 |
7 | export class KakaoTokenRepository {
8 | constructor(
9 | @InjectRedis(DEFAULT_REDIS_NAMESPACE) private readonly redis: Redis,
10 | private readonly kakaoService: KakaoService,
11 | ) {}
12 |
13 | async save(userId: number, kakaoToken: KakaoToken): Promise {
14 | const { accessTokenKey, refreshTokenKey } = this.getTokenKey(userId);
15 |
16 | await this.redis.set(
17 | accessTokenKey,
18 | kakaoToken.getAcessToken(),
19 | 'EX',
20 | kakaoToken.getExpiresIn(),
21 | );
22 |
23 | await this.redis.set(
24 | refreshTokenKey,
25 | kakaoToken.getRefreshToken(),
26 | 'EX',
27 | kakaoToken.getRefreshTokenExpiresIn(),
28 | );
29 | }
30 |
31 | async findByUserId(userId: number): Promise {
32 | const { accessTokenKey, refreshTokenKey } = this.getTokenKey(userId);
33 | const acessToken = await this.redis.get(accessTokenKey);
34 | const expiresIn = await this.redis.ttl(accessTokenKey);
35 | const refreshToken = await this.redis.get(refreshTokenKey);
36 | const refreshTokenExpiresIn = await this.redis.ttl(refreshTokenKey);
37 | if (acessToken === null) {
38 | try {
39 | const { access_token, expires_in } =
40 | await this.kakaoService.refreshKakaoAccessToken(refreshToken);
41 | await this.save(
42 | userId,
43 | new KakaoToken(
44 | access_token,
45 | refreshToken,
46 | expires_in,
47 | refreshTokenExpiresIn,
48 | ),
49 | );
50 | } catch (e) {
51 | throw new InternalServerErrorException({
52 | message: 'Cannot refresh Kakao access token',
53 | error: 'KAKAO_REFRESH_TOKEN_ERROR',
54 | });
55 | }
56 | }
57 |
58 | return new KakaoToken(
59 | acessToken,
60 | refreshToken,
61 | expiresIn,
62 | refreshTokenExpiresIn,
63 | );
64 | }
65 |
66 | private getTokenKey(userId: number): {
67 | accessTokenKey: string;
68 | refreshTokenKey: string;
69 | } {
70 | return {
71 | accessTokenKey: `${userId.toString()}:accessToken`,
72 | refreshTokenKey: `${userId.toString()}:refreshToken`,
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/requests/createDraftLetter.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsNumber, IsOptional, IsString } from 'class-validator';
3 |
4 | export class CreateDraftLetterDto {
5 | @IsNumber()
6 | @IsOptional()
7 | @ApiProperty({
8 | example: 1,
9 | description:
10 | '유저 id : 받는 유저가 비회원일 경우 입력 X, 받는 유저가 회원일 경우 입력',
11 | })
12 | readonly receiverId?: number;
13 |
14 | @IsString()
15 | @IsOptional()
16 | @ApiProperty({
17 | example: 'Pretty Minusu',
18 | description: '받는 사람 닉네임: 받는 유저가 회원이 아닐 경우 입력',
19 | })
20 | readonly receiverNickname?: string;
21 |
22 | @IsNumber()
23 | @ApiProperty({
24 | example: 1,
25 | description: '상황 id',
26 | })
27 | readonly situationId: number;
28 |
29 | @IsString()
30 | @ApiProperty({
31 | example: '생일 축하해',
32 | description: '편지 제목',
33 | })
34 | readonly title: string;
35 |
36 | @IsString()
37 | @ApiProperty({
38 | example: '생일 축하하고 싶어서 편지를 써봤어',
39 | description: '편지 내용',
40 | })
41 | readonly content: string;
42 | }
43 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/requests/createExternalLetter.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsDateString, IsNumber, IsString } from 'class-validator';
3 |
4 | export class CreateExternalTextLetterDto {
5 | @IsString()
6 | @ApiProperty({
7 | example: '행복한 크리스마스 보내',
8 | description: '편지 제목',
9 | })
10 | readonly title: string;
11 |
12 | @IsString()
13 | @ApiProperty({
14 | example: '솔로지만 크리스마스 잘 보내고',
15 | description: '편지 내용',
16 | })
17 | readonly content: string;
18 |
19 | @IsDateString()
20 | @ApiProperty({
21 | example: '2022-12-25 00:00:00',
22 | description: '편지 받은 날짜',
23 | })
24 | readonly receivedAt: Date;
25 |
26 | @IsString()
27 | @ApiProperty({
28 | example: 'Pretty Minusu',
29 | description: '받는 사람 닉네임: 받는 유저가 회원이 아닐 경우 입력',
30 | })
31 | readonly senderNickname: string;
32 |
33 | @IsNumber()
34 | @ApiProperty({
35 | example: 1,
36 | description: '상황 id',
37 | })
38 | readonly situationId: number;
39 | }
40 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/requests/createExternalLetterImg.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsDateString, IsNumber, IsString, IsUrl } from 'class-validator';
3 |
4 | export class CreateExternalImgLetterDto {
5 | @IsString()
6 | @ApiProperty({
7 | example: '행복한 크리스마스 보내',
8 | description: '편지 제목',
9 | })
10 | readonly title: string;
11 |
12 | @IsUrl()
13 | @ApiProperty({
14 | example: 'https://s3.ggo-geet.com/letter/1.png',
15 | description: '편지 이미지 url',
16 | })
17 | readonly imageUrl: string;
18 |
19 | @IsDateString()
20 | @ApiProperty({
21 | example: '2022-12-25 00:00:00',
22 | description: '편지 받은 날짜',
23 | })
24 | readonly receivedAt: Date;
25 |
26 | @IsString()
27 | @ApiProperty({
28 | example: 'Pretty Minusu',
29 | description: '받는 사람 닉네임: 받는 유저가 회원이 아닐 경우 입력',
30 | })
31 | readonly senderNickname: string;
32 |
33 | @IsNumber()
34 | @ApiProperty({
35 | example: 1,
36 | description: '상황 id',
37 | })
38 | readonly situationId: number;
39 | }
40 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/requests/findAllLetter.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiPropertyOptional } from '@nestjs/swagger';
2 | import { IsDateString, IsOptional } from 'class-validator';
3 | import { PaginationRequest } from 'src/common/paginations/pagination.request';
4 |
5 | export class findAllReceviedLetterDto extends PaginationRequest {
6 | @ApiPropertyOptional({
7 | description: '보낸 사람 ID 리스트',
8 | example: ['1', '2'],
9 | })
10 | @IsOptional()
11 | readonly senders: string[];
12 |
13 | @ApiPropertyOptional({
14 | description: '검색할 상황 ID 리스트',
15 | example: ['1', '2'],
16 | })
17 | @IsOptional()
18 | readonly situations: string[];
19 |
20 | @ApiPropertyOptional({
21 | example: '2022-01-01 00:00:00',
22 | description: '편지 검색 시작 날짜',
23 | })
24 | @IsOptional()
25 | @IsDateString()
26 | readonly startDate: Date;
27 |
28 | @ApiPropertyOptional({
29 | example: '2023-12-25 00:00:00',
30 | description: '편지 검색 시작 날짜',
31 | })
32 | @IsOptional()
33 | @IsDateString()
34 | readonly endDate: Date;
35 |
36 | @ApiPropertyOptional({
37 | description: '날짜순 정렬',
38 | example: 'ASC',
39 | })
40 | @IsOptional()
41 | readonly order: string;
42 | }
43 |
44 | export class findAllSentLetterDto extends PaginationRequest {
45 | @ApiPropertyOptional({
46 | description: '받는 사람 ID 리스트',
47 | example: ['1', '2'],
48 | })
49 | @IsOptional()
50 | readonly receivers: string[];
51 |
52 | @ApiPropertyOptional({
53 | description: '검색할 상황 ID 리스트',
54 | example: ['1', '2'],
55 | })
56 | @IsOptional()
57 | readonly situations: string[];
58 |
59 | @ApiPropertyOptional({
60 | example: '2022-01-01 00:00:00',
61 | description: '편지 검색 시작 날짜',
62 | })
63 | @IsOptional()
64 | @IsDateString()
65 | readonly startDate: Date;
66 |
67 | @ApiPropertyOptional({
68 | example: '2023-12-25 00:00:00',
69 | description: '편지 검색 시작 날짜',
70 | })
71 | @IsOptional()
72 | @IsDateString()
73 | readonly endDate: Date;
74 |
75 | @ApiPropertyOptional({
76 | description: '날짜순 정렬',
77 | example: 'ASC',
78 | })
79 | @IsOptional()
80 | readonly order: string;
81 | }
82 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/requests/kakaoCallback.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumberString, IsString } from 'class-validator';
2 |
3 | export class KakaoMessageCallbackDto {
4 | @IsString()
5 | CHAT_TYPE: string;
6 |
7 | @IsString()
8 | HASH_CHAT_ID: string;
9 |
10 | @IsNumberString()
11 | TEMPLATE_ID: string;
12 |
13 | @IsNumberString()
14 | TEMP_LETTER_ID: string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/responses/letterDetail.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { ReceivedLetter } from 'src/domain/letter/entities/receivedLetter.entity';
3 | import { SendLetter } from 'src/domain/letter/entities/sendLetter.entity';
4 |
5 | export class SendLetterDetailResponseDto {
6 | @ApiProperty({ example: 1, description: '보낸편지 id' })
7 | id: number;
8 |
9 | @ApiProperty({ example: '받는이', description: '받는사람 이름' })
10 | receiverNickname: string;
11 |
12 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '언제 보냈는지' })
13 | sendAt: Date;
14 |
15 | @ApiProperty({ example: '받는이야 생일축하해~', description: '내용' })
16 | content: string;
17 |
18 | @ApiProperty({ example: 1, description: '상황 id' })
19 | situationId: number;
20 |
21 | constructor(letter: SendLetter) {
22 | this.id = letter.id;
23 | this.receiverNickname = letter.receiverNickname;
24 | this.sendAt = letter.sendAt;
25 | this.situationId = letter.letterBody.situationId;
26 | this.content = letter.letterBody.content;
27 | }
28 | }
29 |
30 | export class ReceivedLetterDetailResponseDto {
31 | @ApiProperty({ example: 1, description: '받은 편지 id' })
32 | id: number;
33 |
34 | @ApiProperty({ example: '보낸이', description: '보낸사람 이름' })
35 | senderNickname: string;
36 |
37 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '언제 받았는지' })
38 | receivedAt: Date;
39 |
40 | @ApiProperty({ example: 'text OR image', description: '외부편지 추가 형식' })
41 | type: string;
42 |
43 | @ApiProperty({
44 | example: '파일 경로',
45 | description: '외부 이미지 편지의 이미지 주소',
46 | })
47 | imageContent: string;
48 |
49 | @ApiProperty({ example: '받는이야 생일축하해~', description: '내용' })
50 | content: string;
51 |
52 | @ApiProperty({ example: 1, description: '상황 id' })
53 | situationId: number;
54 |
55 | constructor(letter: ReceivedLetter) {
56 | this.id = letter.id;
57 | this.senderNickname = letter.senderNickname;
58 | this.receivedAt = letter.receivedAt;
59 | this.situationId = letter.letterBody.situationId;
60 | this.content = letter.letterBody.content;
61 | this.type = letter.letterBody.type;
62 | this.imageContent = letter.letterBody.imageContent;
63 | }
64 | }
65 |
66 | export class tempLetterResponseDto {
67 | @ApiProperty({
68 | example: 1,
69 | description: '보낸 편지 id (상대방이 보낼 때 저장하기 때문에)',
70 | })
71 | id: number;
72 |
73 | @ApiProperty({ example: '보낸이', description: '보낸사람 이름' })
74 | senderNickname: string;
75 |
76 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '언제 받았는지' })
77 | receivedAt: Date;
78 |
79 | @ApiProperty({ example: '받는이야 생일축하해~', description: '내용' })
80 | content: string;
81 |
82 | @ApiProperty({ example: 1, description: '상황 id' })
83 | situationId: number;
84 |
85 | constructor(letter: SendLetter) {
86 | this.id = letter.id;
87 | this.senderNickname = letter.sender.nickname;
88 | this.receivedAt = letter.sendAt;
89 | this.situationId = letter.letterBody.situationId;
90 | this.content = letter.letterBody.content;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/responses/letterStorage.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { ReceivedLetter } from 'src/domain/letter/entities/receivedLetter.entity';
3 | import { SendLetter } from 'src/domain/letter/entities/sendLetter.entity';
4 |
5 | export class ReceviedAllResponseDto {
6 | @ApiProperty({ example: 1, description: '받은 편지 id' })
7 | id: number;
8 | @ApiProperty({ example: '보낸이', description: '보낸사람 이름' })
9 | senderNickname: string;
10 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '언제 받았는지' })
11 | receivedAt: Date;
12 | @ApiProperty({ example: '생축', description: '편지 제목' })
13 | title: string;
14 | @ApiProperty({ example: 1, description: '상황 id' })
15 | situationId: number;
16 |
17 | constructor(letter: ReceivedLetter) {
18 | this.id = letter.id;
19 | this.senderNickname = letter.senderNickname;
20 | this.receivedAt = letter.receivedAt;
21 | this.situationId = letter.letterBody.situationId;
22 | this.title = letter.letterBody.title;
23 | }
24 | }
25 |
26 | export class SendAllResponseDto {
27 | @ApiProperty({ example: 1, description: '보낸편지 id' })
28 | id: number;
29 | @ApiProperty({ example: '받는이', description: '받는사람 이름' })
30 | receiverNickname: string;
31 |
32 | @ApiProperty({ example: '2021-01-01 00:00:00', description: '언제 보냈는지' })
33 | sendAt: Date;
34 | @ApiProperty({ example: '생축', description: '편지 제목' })
35 | title: string;
36 |
37 | @ApiProperty({ example: 1, description: '상황 id' })
38 | situationId: number;
39 |
40 | constructor(letter: SendLetter) {
41 | this.id = letter.id;
42 | this.receiverNickname = letter.receiverNickname;
43 | this.sendAt = letter.sendAt;
44 | this.situationId = letter.letterBody.situationId;
45 | this.title = letter.letterBody.title;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/domain/letter/dto/responses/receviedTempLetter.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class ReceviedTempLetterResponseDto {
4 | @ApiProperty({
5 | example: 1,
6 | description: '편지 ID',
7 | })
8 | id: number;
9 |
10 | @ApiProperty({
11 | example: 'Pretty Minusu',
12 | description: '보낸 사람 닉네임',
13 | })
14 | senderNickname: string;
15 |
16 | @ApiProperty({
17 | example: '2022-12-25 00:00:00',
18 | description: '받은 날짜',
19 | })
20 | receivedAt: Date;
21 |
22 | @ApiProperty({
23 | example: '크리스마스 축하해',
24 | description: '편지 내용',
25 | })
26 | content: string;
27 |
28 | @ApiProperty({
29 | example: 1,
30 | description: '상황 ID',
31 | })
32 | situationId: number;
33 | }
34 |
--------------------------------------------------------------------------------
/src/domain/letter/entities/letterBody.entity.ts:
--------------------------------------------------------------------------------
1 | import { Reply } from '../../reply/entities/reply.entity';
2 | import {
3 | Column,
4 | Entity,
5 | JoinColumn,
6 | OneToOne,
7 | PrimaryGeneratedColumn,
8 | } from 'typeorm';
9 | import { LetterType } from '../letter.constants';
10 |
11 | @Entity('letterBody')
12 | export class LetterBody {
13 | @PrimaryGeneratedColumn('increment')
14 | id: number;
15 |
16 | @Column({ type: 'text', nullable: true })
17 | title: string;
18 |
19 | @Column({ type: 'text', nullable: true })
20 | content: string;
21 |
22 | @Column({ type: 'varchar', length: 255, nullable: true })
23 | resultImg: string;
24 |
25 | @Column({ type: 'varchar', length: 255, nullable: false })
26 | accessCode: string;
27 |
28 | @Column({ type: 'varchar', length: 255, nullable: true })
29 | imageContent: string;
30 |
31 | @Column({
32 | type: 'enum',
33 | enum: LetterType,
34 | })
35 | type: LetterType;
36 |
37 | @OneToOne(() => Reply, { cascade: true })
38 | @JoinColumn()
39 | reply: Reply;
40 |
41 | @Column({ type: 'int' })
42 | situationId: number;
43 | }
44 |
--------------------------------------------------------------------------------
/src/domain/letter/entities/receivedLetter.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | DeleteDateColumn,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | OneToOne,
9 | PrimaryGeneratedColumn,
10 | UpdateDateColumn,
11 | } from 'typeorm';
12 | import { User } from '../../users/entities/user.entity';
13 | import { LetterBody } from './letterBody.entity';
14 |
15 | @Entity('receivedLetter')
16 | export class ReceivedLetter {
17 | @PrimaryGeneratedColumn('increment')
18 | id: number;
19 |
20 | @ManyToOne(() => User)
21 | @JoinColumn({
22 | name: 'receiverId',
23 | referencedColumnName: 'id',
24 | })
25 | receiver: User;
26 |
27 | @ManyToOne(() => User)
28 | @JoinColumn({
29 | name: 'senderId',
30 | referencedColumnName: 'id',
31 | })
32 | sender: User;
33 |
34 | @OneToOne(() => LetterBody, { cascade: true })
35 | @JoinColumn({
36 | name: 'letterBodyId',
37 | referencedColumnName: 'id',
38 | })
39 | letterBody: LetterBody;
40 |
41 | @Column({ type: 'varchar', length: '255' })
42 | senderNickname: string;
43 |
44 | @CreateDateColumn()
45 | receivedAt: Date;
46 |
47 | @CreateDateColumn()
48 | createdAt: Date;
49 |
50 | @UpdateDateColumn()
51 | updatedAt: Date;
52 |
53 | @DeleteDateColumn()
54 | deletedAt: Date;
55 | }
56 |
--------------------------------------------------------------------------------
/src/domain/letter/entities/sendLetter.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | DeleteDateColumn,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | OneToOne,
9 | PrimaryGeneratedColumn,
10 | UpdateDateColumn,
11 | } from 'typeorm';
12 | import { User } from '../../users/entities/user.entity';
13 | import { SendLetterStatus } from '../letter.constants';
14 | import { LetterBody } from './letterBody.entity';
15 |
16 | @Entity('sendLetter')
17 | export class SendLetter {
18 | @PrimaryGeneratedColumn('increment')
19 | id: number;
20 |
21 | @ManyToOne(() => User)
22 | @JoinColumn({
23 | name: 'senderId',
24 | referencedColumnName: 'id',
25 | })
26 | sender: User;
27 |
28 | @ManyToOne(() => User)
29 | @JoinColumn({
30 | name: 'receiverId',
31 | referencedColumnName: 'id',
32 | })
33 | receiver: User;
34 |
35 | @Column({
36 | type: 'enum',
37 | enum: SendLetterStatus,
38 | })
39 | status: string;
40 |
41 | @Column({ type: 'varchar', length: '255' })
42 | receiverNickname: string;
43 |
44 | @OneToOne(() => LetterBody, { cascade: true })
45 | @JoinColumn()
46 | letterBody: LetterBody;
47 |
48 | @CreateDateColumn()
49 | sendAt: Date;
50 |
51 | @CreateDateColumn()
52 | createdAt: Date;
53 |
54 | @UpdateDateColumn()
55 | updatedAt: Date;
56 |
57 | @DeleteDateColumn()
58 | deletedAt: Date;
59 | }
60 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.constants.ts:
--------------------------------------------------------------------------------
1 | export enum LetterType {
2 | EXTERNAL = 'text',
3 | EXTERNALIMG = 'image',
4 | INTERNAL = 'received',
5 | }
6 |
7 | export enum SendLetterStatus {
8 | TMP_SAVED = 'tmpSaved',
9 | SENT = 'sent',
10 | }
11 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Get,
5 | HttpCode,
6 | HttpStatus,
7 | Param,
8 | Post,
9 | Query,
10 | UseGuards,
11 | } from '@nestjs/common';
12 | import { ReqUser } from 'src/common/decorators/user.decorators';
13 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
14 | import { User } from 'src/domain/users/entities/user.entity';
15 | import { LetterService } from './letter.service';
16 | import {
17 | ApiBearerAuth,
18 | ApiOperation,
19 | ApiResponse,
20 | ApiTags,
21 | } from '@nestjs/swagger';
22 | import { CreateDraftLetterDto } from './dto/requests/createDraftLetter.request.dto';
23 | import { KakaoMessageCallbackDto } from './dto/requests/kakaoCallback.request.dto';
24 |
25 | @Controller('letters')
26 | export class LetterController {
27 | constructor(private readonly letterService: LetterService) {}
28 |
29 | @ApiTags('Letter API')
30 | @ApiOperation({
31 | summary: '편지 생성 API',
32 | description:
33 | '신규 편지를 생성합니다, 편지를 보내기 위해서는 Complete API를 호출해주세요.',
34 | })
35 | @UseGuards(JwtAuthGuard)
36 | @ApiBearerAuth()
37 | @Post('/draft')
38 | @HttpCode(HttpStatus.CREATED)
39 | async createDraftLetter(
40 | @ReqUser() user: User,
41 | @Body() createSendLetterDto: CreateDraftLetterDto,
42 | ) {
43 | const result = await this.letterService.createDraftLetter(
44 | user,
45 | createSendLetterDto,
46 | );
47 | return { data: result };
48 | }
49 |
50 | @ApiTags('Letter API')
51 | @ApiOperation({
52 | summary: '편지 전송 API',
53 | description: '편지를 친구에게 보냅니다.',
54 | })
55 | @UseGuards(JwtAuthGuard)
56 | @ApiBearerAuth()
57 | @Post(':id/complete')
58 | @HttpCode(HttpStatus.CREATED)
59 | async sendLetter(@ReqUser() user: User, @Param('id') id: number) {
60 | const letter = await this.letterService.sendLetter(user, id);
61 | return { data: letter };
62 | }
63 |
64 | @ApiTags('Letter API')
65 | @ApiOperation({
66 | summary: '편지 외부 전송 API',
67 | description: '편지를 친구가 아닌 외부에 보냅니다.',
68 | })
69 | @ApiResponse({
70 | status: 201,
71 | description: '임시 편지가 성공적으로 생성되었습니다.',
72 | })
73 | @UseGuards(JwtAuthGuard)
74 | @ApiBearerAuth()
75 | @Post(':id/temp-complete')
76 | @HttpCode(HttpStatus.CREATED)
77 | async sendTempLetter(@ReqUser() user: User, @Param('id') id: number) {
78 | return this.letterService.sendTempLetter(user, id);
79 | }
80 |
81 | @ApiTags('Letter API')
82 | @ApiOperation({
83 | summary: '편지 외부 전송 콜백 API',
84 | description: '카카오로 전송된 편지의 Callback을 받습니다.',
85 | })
86 | @Get('/temp-complete/kakao/callback')
87 | @HttpCode(HttpStatus.CREATED)
88 | async getKaKaoTempLetterCallback(@Query() query: KakaoMessageCallbackDto) {
89 | return this.letterService.getKaKaoTempLetterCallback(query);
90 | }
91 |
92 | @ApiTags('Letter API')
93 | @ApiOperation({
94 | summary: '편지 외부 전송 콜백 확인 API',
95 | description: '카카오로 전송된 편지의 Callback을 확인합니다.',
96 | })
97 | @UseGuards(JwtAuthGuard)
98 | @ApiBearerAuth()
99 | @Get(':id/temp-complete/kakao/callback/confirm')
100 | async getKaKaoTempLetterCallbackCheck(@Param('id') id: number) {
101 | return this.letterService.getKaKaoTempLetterCallbackCheck(id);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { MulterModule } from '@nestjs/platform-express';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { User } from 'src/domain/users/entities/user.entity';
6 | import { multerAttachedImgOptionsFactory } from 'src/infrastructor/S3/s3.multer';
7 | import { ReceivedLetter } from './entities/receivedLetter.entity';
8 | import { LetterController } from './letter.controller';
9 | import { LetterService } from './letter.service';
10 | import { PassportModule } from '@nestjs/passport';
11 | import { SendLetter } from './entities/sendLetter.entity';
12 | import { AuthModule } from 'src/auth/auth.module';
13 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
14 | import { LetterSentService } from './letter.sent.service';
15 | import { LetterReceivedService } from './letter.received.service';
16 | import { Friend } from 'src/domain/friend/entities/friend.entity';
17 | import { KakaoService } from 'src/domain/kakao/kakao.service';
18 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
19 | import { RedisModule } from '@liaoliaots/nestjs-redis';
20 | import { TempLetterRepository } from './repository/tempLetter.repository';
21 |
22 | @Module({
23 | imports: [
24 | MulterModule.registerAsync({
25 | imports: [ConfigModule],
26 | useFactory: multerAttachedImgOptionsFactory,
27 | inject: [ConfigService],
28 | }),
29 | TypeOrmModule.forFeature([ReceivedLetter, User, SendLetter, Friend]),
30 | PassportModule,
31 | AuthModule,
32 | RedisModule,
33 | ],
34 | controllers: [LetterController],
35 | providers: [
36 | LetterService,
37 | LetterSentService,
38 | LetterReceivedService,
39 | JwtStrategy,
40 | KakaoService,
41 | KakaoTokenRepository,
42 | TempLetterRepository,
43 | ],
44 | })
45 | export class LetterModule {}
46 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.received.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BadRequestException,
3 | Injectable,
4 | NotFoundException,
5 | } from '@nestjs/common';
6 | import { Repository } from 'typeorm';
7 | import { ReceivedLetter } from './entities/receivedLetter.entity';
8 | import { InjectRepository } from '@nestjs/typeorm';
9 | import { User } from 'src/domain/users/entities/user.entity';
10 | import { CreateExternalImgLetterDto } from './dto/requests/createExternalLetterImg.request.dto';
11 | import { CreateExternalTextLetterDto } from './dto/requests/createExternalLetter.request.dto';
12 | import { LetterBody } from './entities/letterBody.entity';
13 | import { LetterType, SendLetterStatus } from './letter.constants';
14 | import { PaginationBuilder } from 'src/common/paginations/paginationBuilder.response';
15 | import { TempLetterRepository } from './repository/tempLetter.repository';
16 | import { ReceviedTempLetterResponseDto } from './dto/responses/receviedTempLetter.response.dto';
17 | import { ReceviedAllResponseDto } from './dto/responses/letterStorage.response.dto';
18 | import { ReceivedLetterDetailResponseDto } from './dto/responses/letterDetail.response.dto';
19 | import { SendLetter } from './entities/sendLetter.entity';
20 |
21 | @Injectable()
22 | export class LetterReceivedService {
23 | constructor(
24 | @InjectRepository(ReceivedLetter)
25 | private receivedLetterRepository: Repository,
26 | private tempLetterRepository: TempLetterRepository,
27 | @InjectRepository(SendLetter)
28 | private sendLetterRepository: Repository,
29 | ) {}
30 |
31 | async createTextLetter(
32 | user: User,
33 | createExternalTextLetter: CreateExternalTextLetterDto,
34 | ) {
35 | const letterBody = new LetterBody();
36 | letterBody.title = createExternalTextLetter.title;
37 | letterBody.content = createExternalTextLetter.content;
38 | letterBody.type = LetterType.EXTERNAL;
39 | letterBody.situationId = createExternalTextLetter.situationId;
40 |
41 | const receivedLetter = new ReceivedLetter();
42 | receivedLetter.receiver = user;
43 | receivedLetter.letterBody = letterBody;
44 | receivedLetter.senderNickname = createExternalTextLetter.senderNickname;
45 |
46 | const result = await this.receivedLetterRepository.save(receivedLetter);
47 | return result;
48 | }
49 |
50 | async createImageLetter(
51 | user: User,
52 | createExternalImgLetterDto: CreateExternalImgLetterDto,
53 | ) {
54 | const letterBody = new LetterBody();
55 | letterBody.title = createExternalImgLetterDto.title;
56 | letterBody.imageContent = createExternalImgLetterDto.imageUrl;
57 | letterBody.type = LetterType.EXTERNALIMG;
58 | letterBody.situationId = createExternalImgLetterDto.situationId;
59 |
60 | const receivedLetter = new ReceivedLetter();
61 | receivedLetter.receiver = user;
62 | receivedLetter.letterBody = letterBody;
63 | receivedLetter.senderNickname = createExternalImgLetterDto.senderNickname;
64 |
65 | const result = await this.receivedLetterRepository.save(receivedLetter);
66 | return result;
67 | }
68 |
69 | async findAll(user: User, query: any): Promise {
70 | const order = query.order === 'ASC' ? 'ASC' : 'DESC';
71 |
72 | const letter = this.receivedLetterRepository
73 | .createQueryBuilder('receivedLetter')
74 | .leftJoinAndSelect('receivedLetter.letterBody', 'letterBody')
75 | .where('receiverId = :id', { id: user.id });
76 |
77 | if (query.startDate !== undefined && query.endDate !== undefined) {
78 | letter.andWhere('receivedAt BETWEEN :startDate AND :endDate', {
79 | startDate: query.startDate,
80 | endDate: query.endDate,
81 | });
82 | }
83 |
84 | if (query.senders !== undefined) {
85 | letter.andWhere('senderId IN (:senders)', {
86 | senders: query.senders,
87 | });
88 | }
89 |
90 | if (query.situations != undefined) {
91 | letter.andWhere('situationId IN (:situations)', {
92 | situations: query.situations,
93 | });
94 | }
95 |
96 | const [dataList, count] = await letter
97 |
98 | .skip(query.getSkip())
99 | .take(query.getTake())
100 | .orderBy('receivedLetter.receivedAt', order)
101 | .getManyAndCount();
102 | console.log(dataList);
103 | const data = dataList.map((element) => {
104 | return new ReceviedAllResponseDto(element);
105 | });
106 | return new PaginationBuilder()
107 | .setData(data)
108 | .setTotalCount(count)
109 | .setPage(query.getPage())
110 | .setTake(query.getTake())
111 | .build();
112 | }
113 |
114 | async findOne(
115 | user: User,
116 | id: number,
117 | ): Promise {
118 | const letter = await this.receivedLetterRepository.findOne({
119 | where: { id: id, receiver: { id: user.id } },
120 | relations: ['letterBody'],
121 | });
122 | if (!letter) {
123 | throw new NotFoundException({
124 | statusCode: 404,
125 | message: 'This Letter is not available',
126 | error:
127 | "Bad Request to this Letter Id OR You don't have access to this letter.",
128 | });
129 | }
130 |
131 | return new ReceivedLetterDetailResponseDto(letter);
132 | }
133 |
134 | async delete(id: number): Promise {
135 | const result = await this.receivedLetterRepository.softDelete(id);
136 | if (result.affected === 0) {
137 | throw new NotFoundException({
138 | statusCode: 404,
139 | message: 'This Letter is not available',
140 | error:
141 | "Bad Request to this Letter Id OR You don't have access to this letter.",
142 | });
143 | }
144 | }
145 |
146 | async uploadExternalLetterImage(file: Express.MulterS3.File): Promise {
147 | if (!file) {
148 | throw new NotFoundException({
149 | statusCode: 404,
150 | message: 'There is no image file',
151 | error: 'Image file is not uploaded',
152 | });
153 | }
154 | return {
155 | filePath: file.location,
156 | };
157 | }
158 |
159 | async findOneTemp(id: number): Promise {
160 | const isActive = this.tempLetterRepository.findById(id);
161 |
162 | if (!isActive) {
163 | throw new BadRequestException({
164 | message: 'Letter is Already Time Out or Deleted',
165 | error:
166 | 'Your letter access time has expired and you can no longer read it',
167 | });
168 | }
169 |
170 | const sendLetter = await this.sendLetterRepository.findOne({
171 | where: { id: id },
172 | relations: {
173 | receiver: true,
174 | letterBody: true,
175 | sender: true,
176 | },
177 | });
178 |
179 | if (!sendLetter || sendLetter.status == SendLetterStatus.SENT) {
180 | throw new NotFoundException({
181 | message: 'There is no id',
182 | error: 'Bad Request to this Id, There is no id',
183 | });
184 | }
185 |
186 | const result = new ReceviedTempLetterResponseDto();
187 | result.id = id;
188 | result.senderNickname = sendLetter.sender.nickname;
189 | result.receivedAt = sendLetter.createdAt;
190 | result.content = sendLetter.letterBody.content;
191 | result.situationId = sendLetter.letterBody.situationId;
192 |
193 | return result;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.sent.controller.ts:
--------------------------------------------------------------------------------
1 | import { LetterSentService } from './letter.sent.service';
2 | import {
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Query,
10 | UseGuards,
11 | } from '@nestjs/common';
12 | import {
13 | ApiBearerAuth,
14 | ApiOperation,
15 | ApiResponse,
16 | ApiTags,
17 | } from '@nestjs/swagger';
18 | import {
19 | ApiPaginationRequst,
20 | ApiPaginationResponse,
21 | } from '../../common/paginations/pagination.swagger';
22 | import { SendAllResponseDto } from './dto/responses/letterStorage.response.dto';
23 | import { JwtAuthGuard } from '../../common/guards/jwtAuth.guard';
24 | import { ReqUser } from '../../common/decorators/user.decorators';
25 | import { User } from '../users/entities/user.entity';
26 | import { findAllSentLetterDto } from './dto/requests/findAllLetter.request.dto';
27 | import { SendLetterDetailResponseDto } from './dto/responses/letterDetail.response.dto';
28 |
29 | @Controller('letters/sent')
30 | export class LetterSentController {
31 | constructor(private readonly letterSentService: LetterSentService) {}
32 |
33 | @ApiTags('Sent Letter API')
34 | @ApiOperation({
35 | summary: '보낸 편지함 조회 API',
36 | description: '보낸 편지를 조회합니다.',
37 | })
38 | @ApiPaginationRequst()
39 | @ApiPaginationResponse(SendAllResponseDto)
40 | @UseGuards(JwtAuthGuard)
41 | @ApiBearerAuth()
42 | @HttpCode(HttpStatus.OK)
43 | @Get()
44 | async getSendLetter(
45 | @ReqUser() user: User,
46 | @Query() query: findAllSentLetterDto,
47 | ) {
48 | return this.letterSentService.findAll(user, query);
49 | }
50 |
51 | @ApiTags('Sent Letter API')
52 | @ApiOperation({
53 | summary: '보낸 편지 상세 조회 API',
54 | description: '보낸 편지를 상세 조회합니다.',
55 | })
56 | @ApiResponse({
57 | status: HttpStatus.OK,
58 | description: '보낸 편지',
59 | type: SendLetterDetailResponseDto,
60 | })
61 | @UseGuards(JwtAuthGuard)
62 | @ApiBearerAuth()
63 | @Get('/:id')
64 | async getSentLetter(@ReqUser() user: User, @Param('id') id: number) {
65 | const sentLetter = await this.letterSentService.findOne(user, id);
66 | return { data: sentLetter };
67 | }
68 |
69 | @ApiTags('Sent Letter API')
70 | @ApiOperation({
71 | summary: '보낸 편지 삭제하기 API',
72 | description: '보낸 편지를 삭제합니다.',
73 | })
74 | @UseGuards(JwtAuthGuard)
75 | @ApiBearerAuth()
76 | @Delete('/:id')
77 | @HttpCode(HttpStatus.NO_CONTENT)
78 | deleteSentLetter(@ReqUser() user: User, @Param('id') id: number) {
79 | return this.letterSentService.delete(user, id);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.sent.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { User } from 'src/domain/users/entities/user.entity';
4 | import { SendLetter } from './entities/sendLetter.entity';
5 | import { Repository } from 'typeorm';
6 | import { findAllSentLetterDto } from './dto/requests/findAllLetter.request.dto';
7 | import { PaginationBuilder } from 'src/common/paginations/paginationBuilder.response';
8 | import { SendAllResponseDto } from './dto/responses/letterStorage.response.dto';
9 | import { SendLetterDetailResponseDto } from './dto/responses/letterDetail.response.dto';
10 |
11 | @Injectable()
12 | export class LetterSentService {
13 | constructor(
14 | @InjectRepository(SendLetter)
15 | private sendLetterRepository: Repository,
16 | ) {}
17 |
18 | async findAll(
19 | user: User,
20 | query: findAllSentLetterDto,
21 | ): Promise {
22 | const order = query.order === 'ASC' ? 'ASC' : 'DESC';
23 |
24 | const letter = this.sendLetterRepository
25 | .createQueryBuilder('sendLetter')
26 | .where('senderId = :id', { id: user.id });
27 |
28 | if (query.startDate !== undefined && query.endDate !== undefined) {
29 | letter.andWhere('sendAt BETWEEN :startDate AND :endDate', {
30 | startDate: query.startDate,
31 | endDate: query.endDate,
32 | });
33 | }
34 |
35 | if (query.receivers !== undefined) {
36 | letter.andWhere('receiverId IN (:receivers)', {
37 | receivers: query.receivers,
38 | });
39 | }
40 |
41 | if (query.situations != undefined) {
42 | letter.andWhere('situationId IN (:situations)', {
43 | situations: query.situations,
44 | });
45 | }
46 |
47 | const [dataList, count] = await letter
48 | .leftJoinAndSelect('sendLetter.letterBody', 'letterBody')
49 | .skip(query.getSkip())
50 | .take(query.getTake())
51 | .orderBy('sendLetter.sendAt', order)
52 | .getManyAndCount();
53 |
54 | const data = dataList.map((element) => {
55 | return new SendAllResponseDto(element);
56 | });
57 |
58 | return new PaginationBuilder()
59 | .setData(data)
60 | .setTotalCount(count)
61 | .setPage(query.getPage())
62 | .setTake(query.getTake())
63 | .build();
64 | }
65 |
66 | async findOne(user: User, id: number): Promise {
67 | const letter = await this.sendLetterRepository.findOne({
68 | where: { id: id, sender: { id: user.id } },
69 | relations: ['letterBody'],
70 | });
71 | if (!letter) {
72 | throw new NotFoundException({
73 | statusCode: 404,
74 | message: 'This Letter is not available',
75 | error:
76 | "Bad Request to this Letter Id OR You don't have access to this letter.",
77 | });
78 | }
79 | return new SendLetterDetailResponseDto(letter);
80 | }
81 |
82 | async delete(user: User, id: number): Promise {
83 | const result = await this.sendLetterRepository.softDelete(id);
84 | if (result.affected === 0) {
85 | throw new NotFoundException({
86 | statusCode: 404,
87 | message: 'This Letter is not available',
88 | error:
89 | "Bad Request to this Letter Id OR You don't have access to this letter.",
90 | });
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { User } from 'src/domain/users/entities/user.entity';
3 | import { CreateDraftLetterDto } from './dto/requests/createDraftLetter.request.dto';
4 | import { LetterBody } from './entities/letterBody.entity';
5 | import { SendLetter } from './entities/sendLetter.entity';
6 | import { SendLetterStatus } from './letter.constants';
7 | import { InjectRepository } from '@nestjs/typeorm';
8 | import { Repository } from 'typeorm';
9 | import { ReceivedLetter } from './entities/receivedLetter.entity';
10 | import { LetterUtils } from './letter.utils';
11 | import { KakaoTokenRepository } from 'src/domain/kakao/repository/kakaoToken.memory.repository';
12 | import { KakaoToken } from 'src/domain/kakao/kakaoToken';
13 | import { Friend } from 'src/domain/friend/entities/friend.entity';
14 | import { KakaoService } from 'src/domain/kakao/kakao.service';
15 | import { TempLetterRepository } from './repository/tempLetter.repository';
16 | import { KakaoMessageCallbackDto } from './dto/requests/kakaoCallback.request.dto';
17 |
18 | @Injectable()
19 | export class LetterService {
20 | constructor(
21 | @InjectRepository(User)
22 | private userRepository: Repository,
23 | @InjectRepository(SendLetter)
24 | private sendLetterRepository: Repository,
25 | @InjectRepository(ReceivedLetter)
26 | private receivedLetterRepository: Repository,
27 | @InjectRepository(Friend)
28 | private friendRepository: Repository,
29 | private readonly kakaoService: KakaoService,
30 | private readonly kakaoTokenRepository: KakaoTokenRepository,
31 | private readonly tempLetterRepository: TempLetterRepository,
32 | ) {}
33 |
34 | async createDraftLetter(
35 | user: User,
36 | createDraftLetterDto: CreateDraftLetterDto,
37 | ): Promise {
38 | /*
39 | * Check Recevier is Available
40 | * Save Letter Body(title, content, situationId)
41 | * Save Send Letter to Temporarily Saved
42 | */
43 |
44 | if (
45 | createDraftLetterDto?.receiverId == null &&
46 | createDraftLetterDto?.receiverNickname == null
47 | ) {
48 | throw new NotFoundException('Receiver is not available');
49 | }
50 |
51 | const receiver = createDraftLetterDto?.receiverId
52 | ? await this.userRepository.findOne({
53 | where: { id: createDraftLetterDto.receiverId },
54 | })
55 | : null;
56 |
57 | const receiverNickname = createDraftLetterDto?.receiverNickname
58 | ? createDraftLetterDto?.receiverNickname
59 | : receiver?.nickname;
60 |
61 | const letterBody = new LetterBody();
62 | letterBody.title = createDraftLetterDto.title;
63 | letterBody.content = createDraftLetterDto.content;
64 | letterBody.accessCode = LetterUtils.generateAccessCode();
65 | letterBody.situationId = createDraftLetterDto.situationId;
66 |
67 | const sendLetter = new SendLetter();
68 | sendLetter.sender = user;
69 | sendLetter.receiver = receiver;
70 | sendLetter.receiverNickname = receiverNickname;
71 | sendLetter.status = SendLetterStatus.TMP_SAVED;
72 | sendLetter.letterBody = letterBody;
73 |
74 | const result = await this.sendLetterRepository.save(sendLetter);
75 | return result;
76 | }
77 |
78 | async sendLetter(user: User, id: number): Promise {
79 | /*
80 | * Validation of letter IDs and receiver
81 | * Get Access Token through Kakao API
82 | * Create new incoming letters, save sender information, and update the status of outgoing letters
83 | * Send Message to KaKaotalk Friends API
84 | */
85 | const sendLetter = await this.sendLetterRepository.findOne({
86 | where: { id: id, sender: { id: user.id } },
87 | relations: {
88 | receiver: true,
89 | sender: true,
90 | letterBody: true,
91 | },
92 | });
93 |
94 | if (!sendLetter) {
95 | throw new NotFoundException({
96 | statusCode: 404,
97 | message: 'There is no id',
98 | error: 'Bad Request to this Id, There is no id',
99 | });
100 | }
101 | if (sendLetter.receiver == null) {
102 | throw new NotFoundException({
103 | statusCode: 404,
104 | message: 'This Letter is not available',
105 | error: 'Bad Request to this Id, This Letter is not available',
106 | });
107 | }
108 | if (sendLetter.status == SendLetterStatus.SENT) {
109 | throw new NotFoundException({
110 | statusCode: 404,
111 | message: 'This Letter already sent',
112 | error: 'Bad Request to this Id, This Letter already sent',
113 | });
114 | }
115 | if (sendLetter.status == SendLetterStatus.SENT) {
116 | throw new NotFoundException({
117 | statusCode: 404,
118 | message: 'This Letter already sent',
119 | error: 'Bad Request to this Id, This Letter already sent',
120 | });
121 | }
122 |
123 | const kakaoToken: KakaoToken = await this.kakaoTokenRepository.findByUserId(
124 | user.id,
125 | );
126 | const acessToken = kakaoToken.getAcessToken();
127 |
128 | const friend = await this.friendRepository.findOne({
129 | where: {
130 | user: { id: user.id },
131 | friendUser: { id: sendLetter.receiver.id },
132 | },
133 | });
134 |
135 | if (!friend) {
136 | throw new NotFoundException({
137 | statusCode: 404,
138 | message: 'This Friend is not available',
139 | error: 'Bad Request to this Id, This Friend is not available',
140 | });
141 | }
142 |
143 | const kakaoUuid = friend.kakaoUuid;
144 |
145 | const receivedLetter = new ReceivedLetter();
146 | receivedLetter.sender = user;
147 | receivedLetter.receiver = sendLetter.receiver;
148 | receivedLetter.letterBody = sendLetter.letterBody;
149 | receivedLetter.senderNickname = sendLetter.sender.nickname;
150 | await this.receivedLetterRepository.save(receivedLetter);
151 |
152 | await this.sendLetterRepository.update(id, {
153 | status: SendLetterStatus.SENT,
154 | });
155 |
156 | const template_id = 87992;
157 | const template_args = `{\"letterId\": "${receivedLetter.id}"}`;
158 | const result = this.kakaoService.sendKakaoMessage(
159 | acessToken,
160 | kakaoUuid,
161 | template_id,
162 | template_args,
163 | );
164 | return result;
165 | }
166 |
167 | async sendTempLetter(user: User, id: number): Promise {
168 | const sendLetter = await this.sendLetterRepository.findOne({
169 | where: { id: id, sender: { id: user.id } },
170 | relations: {
171 | sender: true,
172 | },
173 | });
174 |
175 | if (!sendLetter) {
176 | throw new NotFoundException({
177 | statusCode: 404,
178 | message: 'There is no id',
179 | error: 'Bad Request to this Id, There is no id',
180 | });
181 | }
182 | if (sendLetter.receiverNickname == null) {
183 | throw new NotFoundException({
184 | statusCode: 404,
185 | message: 'There is no receiverNickname',
186 | error:
187 | 'There is no receiverNickname, If you want to send this letter, please set receiverNickname',
188 | });
189 | }
190 |
191 | await this.tempLetterRepository.save(sendLetter.id);
192 | return {
193 | data: {
194 | tempLetterId: sendLetter.id,
195 | expiredDate: '2023-01-14',
196 | },
197 | };
198 | }
199 |
200 | async getKaKaoTempLetterCallback(query: KakaoMessageCallbackDto) {
201 | const letterId = parseInt(query.TEMP_LETTER_ID);
202 | const sendLetterId = await this.tempLetterRepository.save(letterId, true);
203 | return sendLetterId;
204 | }
205 |
206 | async getKaKaoTempLetterCallbackCheck(id: number) {
207 | const result = await this.tempLetterRepository.findById(id);
208 | return {
209 | data: {
210 | sent: result,
211 | },
212 | };
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/domain/letter/letter.utils.ts:
--------------------------------------------------------------------------------
1 | export class LetterUtils {
2 | public static generateAccessCode(): string {
3 | return this.generateRandomString(6);
4 | }
5 |
6 | private static generateRandomString(size: number): string {
7 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
8 | let result = '';
9 | const charactersLength = characters.length;
10 | for (let i = 0; i < size; i++) {
11 | result += characters.charAt(Math.floor(Math.random() * charactersLength));
12 | }
13 | return result;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/domain/letter/lettter.received.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | Query,
11 | UploadedFile,
12 | UseGuards,
13 | UseInterceptors,
14 | } from '@nestjs/common';
15 | import { LetterReceivedService } from './letter.received.service';
16 | import {
17 | ApiBadRequestResponse,
18 | ApiBearerAuth,
19 | ApiBody,
20 | ApiConsumes,
21 | ApiNotFoundResponse,
22 | ApiOperation,
23 | ApiResponse,
24 | ApiTags,
25 | } from '@nestjs/swagger';
26 | import {
27 | ReceivedLetterDetailResponseDto,
28 | tempLetterResponseDto,
29 | } from './dto/responses/letterDetail.response.dto';
30 | import { JwtAuthGuard } from '../../common/guards/jwtAuth.guard';
31 | import { ReqUser } from '../../common/decorators/user.decorators';
32 | import { User } from '../users/entities/user.entity';
33 | import {
34 | ApiPaginationRequst,
35 | ApiPaginationResponse,
36 | } from '../../common/paginations/pagination.swagger';
37 | import { ReceviedAllResponseDto } from './dto/responses/letterStorage.response.dto';
38 | import { findAllReceviedLetterDto } from './dto/requests/findAllLetter.request.dto';
39 | import { CreateExternalTextLetterDto } from './dto/requests/createExternalLetter.request.dto';
40 | import { CreateExternalImgLetterDto } from './dto/requests/createExternalLetterImg.request.dto';
41 | import { FileInterceptor } from '@nestjs/platform-express';
42 |
43 | @Controller('letters/received')
44 | export class LetterReceivedController {
45 | constructor(private readonly letterReceivedService: LetterReceivedService) {}
46 | @ApiTags('Received Letter API')
47 | @ApiOperation({
48 | summary: '받은 편지함 조회 API',
49 | description: '유저가 받은 편지함 조회을 조회합니다.',
50 | })
51 | @UseGuards(JwtAuthGuard)
52 | @ApiBearerAuth()
53 | @ApiPaginationRequst()
54 | @ApiPaginationResponse(ReceviedAllResponseDto)
55 | @HttpCode(HttpStatus.OK)
56 | @Get()
57 | async findAll(
58 | @ReqUser() user: User,
59 | @Query()
60 | query: findAllReceviedLetterDto,
61 | ) {
62 | return await this.letterReceivedService.findAll(user, query);
63 | }
64 |
65 | @ApiTags('Received Letter API')
66 | @ApiOperation({
67 | summary: '받은 편지 상세 조회 API',
68 | description: '편지 상세 조회를 조회합니다.',
69 | })
70 | @ApiResponse({
71 | status: HttpStatus.OK,
72 | description: '받은 편지',
73 | type: ReceivedLetterDetailResponseDto,
74 | })
75 | @UseGuards(JwtAuthGuard)
76 | @ApiBearerAuth()
77 | @Get('/:id')
78 | async findOne(@ReqUser() user: User, @Param('id') id: number) {
79 | const receivedLetter = await this.letterReceivedService.findOne(user, id);
80 | return { data: receivedLetter };
81 | }
82 |
83 | @ApiTags('Received Letter API')
84 | @ApiOperation({
85 | summary: '받은 편지 삭제 API',
86 | description: '받은 편지를 삭제합니다.',
87 | })
88 | @UseGuards(JwtAuthGuard)
89 | @ApiBearerAuth()
90 | @HttpCode(HttpStatus.NO_CONTENT)
91 | @Delete('/:id')
92 | delete(@Param('id') id: number) {
93 | return this.letterReceivedService.delete(id);
94 | }
95 |
96 | @ApiTags('Received Letter API')
97 | @ApiOperation({
98 | summary: '임시로 받은 편지 조회 API',
99 | description: '임시로 받은 편지를 조회합니다.',
100 | })
101 | @ApiResponse({
102 | status: HttpStatus.OK,
103 | description: '임시로 받은 편지 조회',
104 | type: tempLetterResponseDto,
105 | })
106 | @ApiNotFoundResponse({
107 | status: HttpStatus.NOT_FOUND,
108 | description: '임시로 받은 편지가 없습니다.',
109 | })
110 | @ApiBadRequestResponse({
111 | status: HttpStatus.BAD_REQUEST,
112 | description: '잘못된 요청입니다.',
113 | })
114 | @Get('/temp/:id')
115 | async findOneTemp(@Param('id') id: number) {
116 | const tempReceivedLetter = await this.letterReceivedService.findOneTemp(id);
117 | return { data: tempReceivedLetter };
118 | }
119 |
120 | @ApiTags('Received Letter API')
121 | @ApiOperation({
122 | summary: '외부 텍스트 편지 생성 API',
123 | description: '외부에서 받은 텍스트로 된 편지를 생성합니다.',
124 | })
125 | @UseGuards(JwtAuthGuard)
126 | @ApiBearerAuth()
127 | @Post('/texts')
128 | @HttpCode(HttpStatus.CREATED)
129 | async createExternalTextLetter(
130 | @ReqUser() user: User,
131 | @Body() createExternalTextLetterDto: CreateExternalTextLetterDto,
132 | ) {
133 | const letter = await this.letterReceivedService.createTextLetter(
134 | user,
135 | createExternalTextLetterDto,
136 | );
137 | return { data: letter };
138 | }
139 |
140 | @ApiTags('Received Letter API')
141 | @ApiOperation({
142 | summary: '외부 이미지 편지 생성 API',
143 | description: '외부에서 받은 이미지로 된 편지를 생성합니다.',
144 | })
145 | @UseGuards(JwtAuthGuard)
146 | @ApiBearerAuth()
147 | @Post('/images')
148 | @HttpCode(HttpStatus.CREATED)
149 | async createExternalImageLetter(
150 | @ReqUser() user: User,
151 | @Body() createExternalImgLetterDto: CreateExternalImgLetterDto,
152 | ) {
153 | const letter = await this.letterReceivedService.createImageLetter(
154 | user,
155 | createExternalImgLetterDto,
156 | );
157 | return { data: letter };
158 | }
159 |
160 | @ApiTags('Received Letter API')
161 | @ApiOperation({
162 | summary: '이미지 편지 업로드 API',
163 | description: '외부에서 받은 이미지로 된 편지를 업로드합니다.',
164 | })
165 | @UseGuards(JwtAuthGuard)
166 | @ApiBearerAuth()
167 | @ApiConsumes('multipart/form-data')
168 | @ApiBody({
169 | schema: {
170 | type: 'object',
171 | properties: {
172 | file: {
173 | type: 'string',
174 | format: 'binary',
175 | },
176 | },
177 | },
178 | })
179 | @ApiResponse({
180 | status: HttpStatus.CREATED,
181 | description: '이미지 편지 업로드 성공',
182 | })
183 | @Post('/images/upload')
184 | @UseInterceptors(FileInterceptor('file'))
185 | async uploadExternalImgLetter(@UploadedFile() file: Express.MulterS3.File) {
186 | const letter = await this.letterReceivedService.uploadExternalLetterImage(
187 | file,
188 | );
189 | return { data: letter };
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/domain/letter/repository/tempLetter.repository.ts:
--------------------------------------------------------------------------------
1 | import Redis from 'ioredis';
2 | import { InjectRedis } from '@liaoliaots/nestjs-redis';
3 |
4 | export class TempLetterRepository {
5 | constructor(@InjectRedis('default') private readonly redis: Redis) {}
6 | private readonly ttl = 3600 * 24 * 3;
7 |
8 | async save(id: number, isSent = false): Promise {
9 | const sentResult = isSent ? '1' : '0';
10 | const tempLetterKey = this.getTokenKey(id);
11 | await this.redis.set(tempLetterKey, sentResult, 'EX', this.ttl);
12 | return id;
13 | }
14 |
15 | async findById(id: number): Promise {
16 | const tempLetterKey = this.getTokenKey(id);
17 | const result = await this.redis.get(tempLetterKey);
18 | return result == '1';
19 | }
20 |
21 | private getTokenKey(id: number): string {
22 | return `tempLetter:${id.toString()}`;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/domain/letter/test/letter.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { LetterController } from '../letter.controller';
2 |
3 | describe('LetterController', () => {
4 | let controller: LetterController;
5 |
6 | it('should be defined', () => {
7 | expect(true).toBe(true);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/domain/letter/test/letter.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { LetterService } from '../letter.service';
2 |
3 | describe('LetterService', () => {
4 | let service: LetterService;
5 |
6 | it('should be defined', () => {
7 | expect(true).toBe(true);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/domain/notice/dto/createNotice.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsString } from 'class-validator';
3 |
4 | export class CreateNoticeDto {
5 | @IsString()
6 | @ApiProperty({
7 | example: '반갑습니다.',
8 | description: '공지사항 제목',
9 | })
10 | readonly title: string;
11 |
12 | @IsString()
13 | @ApiProperty({
14 | example: '반갑습니다. 이 서비스는 ...',
15 | description: '공지사항 내용',
16 | })
17 | readonly content: string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/domain/notice/dto/deleteNotice.dto.ts:
--------------------------------------------------------------------------------
1 | export class DeleteNoticeDto {
2 | status: string;
3 | msg: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/domain/notice/dto/updateNotice.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 | import { CreateNoticeDto } from './createNotice.dto';
3 |
4 | export class UpdateNoticeDto extends PartialType(CreateNoticeDto) {}
5 |
--------------------------------------------------------------------------------
/src/domain/notice/entities/notice.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | Entity,
5 | PrimaryGeneratedColumn,
6 | UpdateDateColumn,
7 | } from 'typeorm';
8 |
9 | @Entity('notice')
10 | export class Notice {
11 | @PrimaryGeneratedColumn()
12 | id: number;
13 |
14 | @Column({ type: 'varchar', length: 255 })
15 | title: string;
16 |
17 | @Column({ type: 'text' })
18 | content: string;
19 |
20 | @CreateDateColumn()
21 | createdAt: Date;
22 |
23 | @UpdateDateColumn()
24 | updatedAt: Date;
25 | }
26 |
--------------------------------------------------------------------------------
/src/domain/notice/notice.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Patch,
10 | Post,
11 | } from '@nestjs/common';
12 | import { CreateNoticeDto } from './dto/createNotice.dto';
13 | import { UpdateNoticeDto } from './dto/updateNotice.dto';
14 | import { NoticeService } from './notice.service';
15 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
16 | import { GetPagination } from 'src/common/decorators/pagination.decorators';
17 | import {
18 | ApiPaginationRequst,
19 | ApiPaginationResponse,
20 | } from 'src/common/paginations/pagination.swagger';
21 | import { Notice } from './entities/notice.entity';
22 | import { PaginationRequest } from 'src/common/paginations/pagination.request';
23 |
24 | @Controller('notices')
25 | @ApiTags('Notice API')
26 | export class NoticeController {
27 | constructor(private readonly noticeService: NoticeService) {}
28 |
29 | @ApiOperation({
30 | summary: '모든 공지사항 가져오기 API',
31 | description: '모든 공지사항을 가져옵니다.',
32 | })
33 | @ApiPaginationRequst()
34 | @ApiPaginationResponse(Notice)
35 | @Get()
36 | findAll(@GetPagination() pagenation: PaginationRequest) {
37 | return this.noticeService.findAll(pagenation);
38 | }
39 |
40 | @ApiOperation({
41 | summary: '공지사항 상세정보 가져오기 API',
42 | description: '공지사항을 상세정보를 가져옵니다.',
43 | })
44 | @Get('/:id')
45 | async findOne(@Param('id') id: number) {
46 | const notice = await this.noticeService.findOne(id);
47 | return { data: notice };
48 | }
49 |
50 | @ApiOperation({
51 | summary: '공지사항 추가하기 API',
52 | description: '공지사항을 추가합니다.',
53 | })
54 | @ApiBearerAuth()
55 | @Post()
56 | @HttpCode(HttpStatus.CREATED)
57 | async create(@Body() noticeData: CreateNoticeDto) {
58 | // todo: add validation
59 | const notice = await this.noticeService.create(noticeData);
60 | return { data: notice };
61 | }
62 |
63 | @ApiOperation({
64 | summary: '공지사항 수정하기 API',
65 | description: '공지사항을 수정합니다.',
66 | })
67 | @ApiBearerAuth()
68 | @Patch(':id')
69 | async update(@Param('id') id: number, @Body() noticeDto: UpdateNoticeDto) {
70 | // todo: add validation
71 | const notice = await this.noticeService.update(id, noticeDto);
72 | return { data: notice };
73 | }
74 |
75 | @ApiOperation({
76 | summary: '공지사항 삭제하기 API',
77 | description: '공지사항을 삭제합니다.',
78 | })
79 | @ApiBearerAuth()
80 | @Delete(':id')
81 | @HttpCode(HttpStatus.NO_CONTENT)
82 | delete(@Param('id') id: number) {
83 | // todo: add validation
84 | return this.noticeService.delete(id);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/domain/notice/notice.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { NoticeController } from './notice.controller';
3 | import { NoticeService } from './notice.service';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { Notice } from './entities/notice.entity';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Notice])],
9 | controllers: [NoticeController],
10 | providers: [NoticeService],
11 | })
12 | export class NoticeModule {}
13 |
--------------------------------------------------------------------------------
/src/domain/notice/notice.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { CreateNoticeDto } from './dto/createNotice.dto';
5 | import { DeleteNoticeDto } from './dto/deleteNotice.dto';
6 | import { UpdateNoticeDto } from './dto/updateNotice.dto';
7 | import { Notice } from './entities/notice.entity';
8 | import { PaginationRequest } from 'src/common/paginations/pagination.request';
9 | import { PaginationResponse } from 'src/common/paginations/pagination.response';
10 | import { PaginationBuilder } from 'src/common/paginations/paginationBuilder.response';
11 |
12 | @Injectable()
13 | export class NoticeService {
14 | constructor(
15 | @InjectRepository(Notice)
16 | private noticeRepository: Repository,
17 | ) {}
18 |
19 | async findAll(
20 | pagenation: PaginationRequest,
21 | ): Promise> {
22 | const [data, count] = await this.noticeRepository.findAndCount({
23 | skip: pagenation.getSkip(),
24 | take: pagenation.getTake(),
25 | });
26 |
27 | return new PaginationBuilder()
28 | .setData(data)
29 | .setPage(pagenation.page)
30 | .setTake(pagenation.take)
31 | .setTotalCount(count)
32 | .build();
33 | }
34 |
35 | async findOne(id: number): Promise {
36 | const notice = await this.noticeRepository.findOneBy({ id });
37 | if (!notice) {
38 | throw new NotFoundException(`Notice #${id} not found`);
39 | }
40 | return notice;
41 | }
42 |
43 | async create(noticeData: CreateNoticeDto): Promise {
44 | const notice = new Notice();
45 | notice.title = noticeData.title;
46 | notice.content = noticeData.content;
47 | return await this.noticeRepository.save(notice);
48 | }
49 |
50 | async update(id: number, noticeData: UpdateNoticeDto): Promise {
51 | const notice = await this.noticeRepository.findOneBy({ id });
52 |
53 | if (!notice) {
54 | throw new NotFoundException(`Notice #${id} not found`);
55 | }
56 |
57 | notice.title = noticeData.title;
58 | notice.content = noticeData.content;
59 |
60 | return await this.noticeRepository.save(notice);
61 | }
62 |
63 | async delete(id: number): Promise {
64 | const notice = await this.noticeRepository.findOneBy({ id });
65 |
66 | if (!notice) {
67 | throw new NotFoundException(`Notice #${id} not found`);
68 | }
69 |
70 | await this.noticeRepository.delete(id);
71 | const result = new DeleteNoticeDto();
72 | result.status = 'success';
73 | result.msg = `Sentence #${id} deleted`;
74 | return result;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/domain/notice/test/notice.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { DeleteNoticeDto } from '../dto/deleteNotice.dto';
3 | import { Notice } from '../entities/notice.entity';
4 | import { NoticeController } from '../notice.controller';
5 | import { NoticeService } from '../notice.service';
6 |
7 | const mockRepository = () => ({});
8 |
9 | describe('NoticeController', () => {
10 | let noticeController: NoticeController;
11 | let noticeService: NoticeService;
12 | let notice: Notice;
13 | let deleteNoticeDto: DeleteNoticeDto;
14 |
15 | beforeEach(async () => {
16 | const moduleRef = await Test.createTestingModule({
17 | controllers: [NoticeController],
18 | providers: [
19 | NoticeService,
20 | {
21 | provide: 'NoticeRepository',
22 | useFactory: mockRepository,
23 | },
24 | ],
25 | }).compile();
26 |
27 | // add dependency of notice service
28 |
29 | noticeService = moduleRef.get(NoticeService);
30 | noticeController = moduleRef.get(NoticeController);
31 | notice = new Notice();
32 | });
33 |
34 | describe('Test FindAll', () => {
35 | it('should return an array of notice', async () => {
36 | const result = [notice];
37 | jest
38 | .spyOn(noticeService, 'findAll')
39 | .mockImplementation(() => Promise.resolve(result));
40 |
41 | expect(await noticeController.findAll()).toBe(result);
42 | });
43 | });
44 |
45 | describe('Test FindOne', () => {
46 | it('should return a notice', async () => {
47 | jest
48 | .spyOn(noticeService, 'findOne')
49 | .mockImplementation(() => Promise.resolve(notice));
50 |
51 | expect(await noticeController.findOne(1)).toBe(notice);
52 | });
53 | });
54 |
55 | describe('Test Create', () => {
56 | it('should create a notice', async () => {
57 | jest
58 | .spyOn(noticeService, 'create')
59 | .mockImplementation(() => Promise.resolve(notice));
60 |
61 | expect(await noticeController.create(notice)).toBe(notice);
62 | });
63 | });
64 |
65 | describe('Test Update', () => {
66 | it('should update a notice', async () => {
67 | jest
68 | .spyOn(noticeService, 'update')
69 | .mockImplementation(() => Promise.resolve(notice));
70 |
71 | expect(await noticeController.update(1, notice)).toBe(notice);
72 | });
73 | });
74 |
75 | describe('Test Remove', () => {
76 | it('should remove a notice', async () => {
77 | jest
78 | .spyOn(noticeService, 'delete')
79 | .mockImplementation(() => Promise.resolve(deleteNoticeDto));
80 |
81 | expect(await noticeController.delete(1)).toBe(undefined);
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/domain/notice/test/notice.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { Repository } from 'typeorm';
3 | import { NoticeService } from '../notice.service';
4 | import { Notice } from '../entities/notice.entity';
5 | import { NotFoundException } from '@nestjs/common';
6 |
7 | const mockRepository = () => ({
8 | find: jest.fn(),
9 | findOne: jest.fn(),
10 | findOneBy: jest.fn(),
11 | save: jest.fn(),
12 | create: jest.fn(),
13 | });
14 |
15 | describe('NoticeService', () => {
16 | let noticeService: NoticeService;
17 | let noticeRepository: Repository;
18 | let notice: Notice;
19 |
20 | beforeEach(async () => {
21 | const moduleRef = await Test.createTestingModule({
22 | providers: [
23 | NoticeService,
24 | {
25 | provide: 'NoticeRepository',
26 | useFactory: mockRepository,
27 | },
28 | ],
29 | }).compile();
30 |
31 | noticeService = moduleRef.get(NoticeService);
32 | noticeRepository = moduleRef.get>('NoticeRepository');
33 | notice = new Notice();
34 | });
35 |
36 | describe('Test FindAll', () => {
37 | it('should return an array of notice', async () => {
38 | const result = [notice];
39 | jest
40 | .spyOn(noticeRepository, 'find')
41 | .mockImplementation(() => Promise.resolve(result));
42 |
43 | expect(await noticeService.findAll()).toBe(result);
44 | });
45 | });
46 |
47 | describe('Test FindOne', () => {
48 | it('should return a notice', async () => {
49 | jest
50 | .spyOn(noticeRepository, 'findOneBy')
51 | .mockImplementation(() => Promise.resolve(notice));
52 |
53 | expect(await noticeService.findOne(1)).toBe(notice);
54 | });
55 | it('If Notice Not Found', async () => {
56 | jest
57 | .spyOn(noticeRepository, 'findOneBy')
58 | .mockImplementation(() => Promise.resolve(undefined));
59 |
60 | await expect(noticeService.findOne(1)).rejects.toThrowError(
61 | NotFoundException,
62 | );
63 | });
64 | });
65 |
66 | describe('Test Create', () => {
67 | it('should create a notice', async () => {
68 | jest
69 | .spyOn(noticeRepository, 'save')
70 | .mockImplementation(() => Promise.resolve(notice));
71 |
72 | expect(await noticeService.create(notice)).toBe(notice);
73 | });
74 | });
75 |
76 | describe('Test Update', () => {
77 | it('should update a notice', async () => {
78 | jest
79 | .spyOn(noticeRepository, 'findOneBy')
80 | .mockImplementation(() => Promise.resolve(notice));
81 | jest
82 | .spyOn(noticeRepository, 'save')
83 | .mockImplementation(() => Promise.resolve(notice));
84 |
85 | expect(await noticeService.update(1, notice)).toBe(notice);
86 | });
87 | it('If Notice id Not Found', async () => {
88 | jest
89 | .spyOn(noticeRepository, 'findOneBy')
90 | .mockImplementation(() => Promise.resolve(undefined));
91 |
92 | await expect(noticeService.update(1, notice)).rejects.toThrowError(
93 | NotFoundException,
94 | );
95 | });
96 | });
97 |
98 | describe('Test Delete', () => {
99 | it('If Notice id Not Found', async () => {
100 | jest
101 | .spyOn(noticeRepository, 'findOneBy')
102 | .mockImplementation(() => Promise.resolve(undefined));
103 |
104 | await expect(noticeService.delete(1)).rejects.toThrowError(
105 | NotFoundException,
106 | );
107 | });
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/src/domain/relationship/entities/relationship.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity('relationship')
4 | export class Relationship {
5 | @PrimaryGeneratedColumn('increment')
6 | id: number;
7 |
8 | @Column({ type: 'text', nullable: true })
9 | content: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/domain/reminder/dto/requests/createReminder.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsBoolean, IsDateString, IsNumber, IsString } from 'class-validator';
3 |
4 | export class CreateReminderDto {
5 | @IsString()
6 | @ApiProperty({
7 | example: '친구 생일 축하',
8 | description: '리마인더 제목',
9 | })
10 | readonly title: string;
11 |
12 | @IsString()
13 | @ApiProperty({
14 | example: '친구 생일 축하 편지 작성하기',
15 | description: '리마인더 내용',
16 | })
17 | readonly content: string;
18 |
19 | @IsNumber()
20 | @ApiProperty({
21 | example: 1,
22 | description: '리마인더 상황 id',
23 | })
24 | readonly situationId: number;
25 |
26 | @IsDateString()
27 | @ApiProperty({
28 | example: '2021-01-01 00:00:00',
29 | description: '이벤트 발생 시간',
30 | })
31 | readonly eventAt: Date;
32 |
33 | @IsBoolean()
34 | @ApiProperty({
35 | example: true,
36 | description: '알림 여부',
37 | })
38 | readonly alertOn: boolean;
39 |
40 | @IsDateString()
41 | @ApiProperty({
42 | example: '2021-01-01 00:00:00',
43 | description: '리마인더 알림 시간',
44 | })
45 | readonly alarmAt: Date;
46 | }
47 |
--------------------------------------------------------------------------------
/src/domain/reminder/dto/requests/findAllReminder.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Transform } from 'class-transformer';
3 | import { IsBoolean, IsOptional } from 'class-validator';
4 | import { PaginationRequest } from 'src/common/paginations/pagination.request';
5 |
6 | export class FindAllReminderQueryDto extends PaginationRequest {
7 | @ApiProperty({ required: false, description: '완료 여부' })
8 | @IsOptional()
9 | @IsBoolean()
10 | @Transform(({ value }) => {
11 | if (value === 'true') return true;
12 | if (value === 'false') return false;
13 | return value;
14 | })
15 | done?: boolean;
16 | }
17 |
--------------------------------------------------------------------------------
/src/domain/reminder/dto/requests/updateReminder.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 | import { CreateReminderDto } from './createReminder.request.dto';
3 |
4 | export class UpdateReminderDto extends PartialType(CreateReminderDto) {}
5 |
--------------------------------------------------------------------------------
/src/domain/reminder/dto/responses/reminder.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Reminder } from 'src/domain/reminder/entities/reminder.entity';
3 |
4 | export class ReminderResponseDto {
5 | @ApiProperty({
6 | example: 1,
7 | description: 'Reminder id',
8 | })
9 | id: number;
10 |
11 | @ApiProperty({
12 | example: '친구 생일 축하',
13 | description: '리마인더 제목',
14 | })
15 | title: string;
16 |
17 | @ApiProperty({
18 | example: '친구 생일 축하 편지 작성하기',
19 | description: '리마인더 내용',
20 | })
21 | content: string;
22 |
23 | @ApiProperty({
24 | example: '2021-01-01 00:00:00',
25 | description: '이벤트 발생 시간',
26 | })
27 | eventAt: Date;
28 |
29 | @ApiProperty({
30 | example: true,
31 | description: '알림 여부',
32 | })
33 | alertOn: boolean;
34 |
35 | @ApiProperty({
36 | example: '2021-01-01 00:00:00',
37 | description: '리마인더 알림 시간',
38 | })
39 | alarmAt: Date;
40 |
41 | @ApiProperty({
42 | example: true,
43 | description: '완료 여부',
44 | })
45 | isDone: boolean;
46 |
47 | @ApiProperty({
48 | example: 1,
49 | description: '상황 ID',
50 | })
51 | situationId: number;
52 |
53 | constructor(reminder: Reminder) {
54 | this.id = reminder.id;
55 | this.title = reminder.title;
56 | this.content = reminder.content;
57 | this.eventAt = reminder.eventAt;
58 | this.alertOn = reminder.alertOn;
59 | this.alarmAt = reminder.alarmAt;
60 | this.isDone = reminder.isDone;
61 | this.situationId = reminder.situationId;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/domain/reminder/dto/responses/reminderStatus.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class ReminderStautsResponseDto {
4 | @ApiProperty({
5 | example: 1,
6 | description: 'Reminder id',
7 | })
8 | id: number;
9 |
10 | @ApiProperty({
11 | example: true,
12 | description: '완료 여부',
13 | })
14 | isDone: boolean;
15 |
16 | constructor(id: number, isDone: boolean) {
17 | this.id = id;
18 | this.isDone = isDone;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/domain/reminder/entities/reminder.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | DeleteDateColumn,
5 | Entity,
6 | JoinColumn,
7 | ManyToOne,
8 | PrimaryGeneratedColumn,
9 | UpdateDateColumn,
10 | } from 'typeorm';
11 | import { User } from '../../users/entities/user.entity';
12 |
13 | @Entity('reminder')
14 | export class Reminder {
15 | @PrimaryGeneratedColumn()
16 | id: number;
17 |
18 | @Column({ type: 'varchar', length: 255 })
19 | title: string;
20 |
21 | @Column({ type: 'text' })
22 | content: string;
23 |
24 | @Column({ type: 'boolean' })
25 | alertOn: boolean;
26 |
27 | @Column({ type: 'boolean' })
28 | isDone: boolean;
29 |
30 | @Column({ type: 'timestamp' })
31 | alarmAt: Date;
32 |
33 | @Column({ type: 'timestamp' })
34 | eventAt: Date;
35 |
36 | @CreateDateColumn()
37 | createdAt: Date;
38 |
39 | @UpdateDateColumn()
40 | updatedAt: Date;
41 |
42 | @DeleteDateColumn()
43 | deletedAt: Date;
44 |
45 | @ManyToOne(() => User)
46 | @JoinColumn({ name: 'userId' })
47 | user: User;
48 |
49 | @Column({ type: 'int' })
50 | situationId: number;
51 | }
52 |
--------------------------------------------------------------------------------
/src/domain/reminder/reminder.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Patch,
10 | Post,
11 | Query,
12 | UseGuards,
13 | } from '@nestjs/common';
14 | import { ReqUser } from 'src/common/decorators/user.decorators';
15 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
16 | import { User } from 'src/domain/users/entities/user.entity';
17 | import { CreateReminderDto } from './dto/requests/createReminder.request.dto';
18 | import { UpdateReminderDto } from './dto/requests/updateReminder.request.dto';
19 | import { ReminderService } from './reminder.service';
20 | import {
21 | ApiBearerAuth,
22 | ApiBody,
23 | ApiForbiddenResponse,
24 | ApiNotFoundResponse,
25 | ApiOperation,
26 | ApiResponse,
27 | ApiTags,
28 | } from '@nestjs/swagger';
29 | import { ReminderResponseDto } from './dto/responses/reminder.response.dto';
30 | import { ReminderStautsResponseDto } from './dto/responses/reminderStatus.response.dto';
31 | import { FindAllReminderQueryDto } from './dto/requests/findAllReminder.request.dto';
32 | import {
33 | ApiPaginationRequst,
34 | ApiPaginationResponse,
35 | } from 'src/common/paginations/pagination.swagger';
36 |
37 | @Controller('reminders')
38 | @ApiTags('Reminder API')
39 | @ApiBearerAuth()
40 | @UseGuards(JwtAuthGuard)
41 | @ApiNotFoundResponse({
42 | description: '리마인더가 존재하지 않습니다.',
43 | })
44 | @ApiForbiddenResponse({
45 | description: '리마인더를 수정 또는 생성, 조회할 권한이 없습니다.',
46 | })
47 | export class ReminderController {
48 | constructor(private readonly reminderService: ReminderService) {}
49 |
50 | @ApiOperation({
51 | summary: '리마인더 목록 조회 API',
52 | description: '유저의 리마인더 목록을 조회합니다.',
53 | })
54 | @ApiPaginationRequst()
55 | @ApiPaginationResponse(ReminderResponseDto)
56 | @Get()
57 | findAll(@Query() query: FindAllReminderQueryDto, @ReqUser() user: User) {
58 | return this.reminderService.findAll(query, user);
59 | }
60 |
61 | @ApiOperation({
62 | summary: '리마인더 상세 조회 API',
63 | description: '유저 리마인더를 상세내용을 조회합니다.',
64 | })
65 | @ApiResponse({
66 | status: HttpStatus.OK,
67 | description: '리마인더 상세내용을 반환합니다.',
68 | type: ReminderResponseDto,
69 | })
70 | @Get(':id')
71 | async findOne(@Param('id') id: number, @ReqUser() user: User) {
72 | const reminder = await this.reminderService.findOne(id, user);
73 | return { data: reminder };
74 | }
75 |
76 | @ApiOperation({
77 | summary: '리마인더 생성 API',
78 | description: '리마인더를 생성합니다.',
79 | })
80 | @ApiResponse({
81 | status: HttpStatus.CREATED,
82 | description: '리마인더 생성 성공',
83 | type: ReminderResponseDto,
84 | })
85 | @ApiBody({
86 | type: CreateReminderDto,
87 | })
88 | @Post()
89 | @HttpCode(HttpStatus.CREATED)
90 | async create(@Body() reminderDto: CreateReminderDto, @ReqUser() user: User) {
91 | const reminder = await this.reminderService.createReminder(
92 | reminderDto,
93 | user,
94 | );
95 | return { data: reminder };
96 | }
97 |
98 | @ApiOperation({
99 | summary: '리마인더 수정 API',
100 | description: '리마인더를 수정합니다.',
101 | })
102 | @ApiResponse({
103 | status: HttpStatus.OK,
104 | description: '리마인더 수정 성공후 상세내용을 반환합니다.',
105 | type: ReminderResponseDto,
106 | })
107 | @ApiBody({
108 | type: UpdateReminderDto,
109 | })
110 | @Patch(':id')
111 | async update(
112 | @Param('id') id: number,
113 | @Body() reminderDto: UpdateReminderDto,
114 | @ReqUser() user: User,
115 | ) {
116 | const reminder = await this.reminderService.update(id, reminderDto, user);
117 | return { data: reminder };
118 | }
119 |
120 | @ApiOperation({
121 | summary: '리마인더 완료 API',
122 | description: '리마인더의 상태를 완료로 변경합니다.',
123 | })
124 | @ApiResponse({
125 | status: HttpStatus.OK,
126 | description: '리마인더 완료 상태변경 성공',
127 | type: ReminderStautsResponseDto,
128 | })
129 | @Patch('done/:id')
130 | async done(@Param('id') id: number, @ReqUser() user: User) {
131 | const reminder = await this.reminderService.done(id, user);
132 | return { data: reminder };
133 | }
134 |
135 | @ApiOperation({
136 | summary: '리마인더 미완료 API',
137 | description: '리마인더의 상태를 미완료로 변경합니다.',
138 | })
139 | @ApiResponse({
140 | status: HttpStatus.OK,
141 | description: '리마인더 미완료 상태변경 성공',
142 | type: ReminderStautsResponseDto,
143 | })
144 | @Patch('undone/:id')
145 | async undone(@Param('id') id: number, @ReqUser() user: User) {
146 | const reminder = await this.reminderService.undone(id, user);
147 | return { data: reminder };
148 | }
149 |
150 | @ApiOperation({
151 | summary: '리마인더 삭제 API',
152 | description: '리마인더를 삭제합니다.',
153 | })
154 | @ApiResponse({
155 | status: HttpStatus.NO_CONTENT,
156 | description: '리마인더 삭제 성공',
157 | })
158 | @HttpCode(HttpStatus.NO_CONTENT)
159 | @Delete(':id')
160 | delete(@Param('id') id: number, @ReqUser() user: User) {
161 | return this.reminderService.delete(id, user);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/domain/reminder/reminder.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PassportModule } from '@nestjs/passport';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { AuthModule } from 'src/auth/auth.module';
5 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
6 | import { User } from 'src/domain/users/entities/user.entity';
7 | import { Reminder } from './entities/reminder.entity';
8 | import { ReminderController } from './reminder.controller';
9 | import { ReminderService } from './reminder.service';
10 |
11 | @Module({
12 | imports: [
13 | TypeOrmModule.forFeature([Reminder, User]),
14 | PassportModule,
15 | AuthModule,
16 | ],
17 | controllers: [ReminderController],
18 | providers: [ReminderService, JwtStrategy],
19 | })
20 | export class ReminderModule {}
21 |
--------------------------------------------------------------------------------
/src/domain/reminder/reminder.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { User } from 'src/domain/users/entities/user.entity';
4 | import { Repository } from 'typeorm';
5 | import { CreateReminderDto } from './dto/requests/createReminder.request.dto';
6 | import { Reminder } from './entities/reminder.entity';
7 | import { UpdateReminderDto } from './dto/requests/updateReminder.request.dto';
8 | import { ReminderResponseDto } from './dto/responses/reminder.response.dto';
9 | import { ReminderStautsResponseDto } from './dto/responses/reminderStatus.response.dto';
10 | import { FindAllReminderQueryDto } from './dto/requests/findAllReminder.request.dto';
11 | import { PaginationBuilder } from 'src/common/paginations/paginationBuilder.response';
12 |
13 | @Injectable()
14 | export class ReminderService {
15 | constructor(
16 | @InjectRepository(Reminder)
17 | private reminderRepository: Repository,
18 | ) {}
19 |
20 | async createReminder(reminderDto: CreateReminderDto, user: User) {
21 | const reminder = new Reminder();
22 | reminder.title = reminderDto.title;
23 | reminder.content = reminderDto.content;
24 | reminder.eventAt = reminderDto.eventAt;
25 | reminder.alertOn = reminderDto.alertOn;
26 | reminder.alarmAt = reminderDto.alarmAt;
27 | reminder.isDone = false;
28 | reminder.user = user;
29 | reminder.situationId = reminderDto.situationId;
30 | await this.reminderRepository.save(reminder);
31 | return new ReminderResponseDto(reminder);
32 | }
33 |
34 | async findAll(query: FindAllReminderQueryDto, user: User) {
35 | const constraint = {
36 | user: {
37 | id: user.id,
38 | },
39 | };
40 |
41 | if (query.done !== undefined) {
42 | constraint['isDone'] = query.done;
43 | }
44 |
45 | const take = query.getTake();
46 | const skip = query.getSkip();
47 |
48 | const [data, count] = await this.reminderRepository.findAndCount({
49 | where: constraint,
50 | skip: skip,
51 | take: take,
52 | select: [
53 | 'id',
54 | 'title',
55 | 'content',
56 | 'eventAt',
57 | 'alertOn',
58 | 'isDone',
59 | 'situationId',
60 | ],
61 | });
62 |
63 | return new PaginationBuilder()
64 | .setData(data)
65 | .setTotalCount(count)
66 | .setPage(query.getPage())
67 | .setTake(query.getTake())
68 | .build();
69 | }
70 |
71 | async findOne(id: number, user: User): Promise {
72 | const reminder = await this.reminderRepository.findOne({
73 | where: {
74 | id: id,
75 | user: {
76 | id: user.id,
77 | },
78 | },
79 | select: [
80 | 'id',
81 | 'title',
82 | 'content',
83 | 'eventAt',
84 | 'alertOn',
85 | 'alarmAt',
86 | 'isDone',
87 | 'situationId',
88 | ],
89 | });
90 | if (!reminder) {
91 | throw new NotFoundException('Reminder not found');
92 | }
93 |
94 | return new ReminderResponseDto(reminder);
95 | }
96 |
97 | async update(
98 | id: number,
99 | updateReminderDto: UpdateReminderDto,
100 | user: User,
101 | ): Promise {
102 | const reminder = await this.reminderRepository.findOne({
103 | where: {
104 | id: id,
105 | user: {
106 | id: user.id,
107 | },
108 | },
109 | });
110 |
111 | if (!reminder) {
112 | throw new NotFoundException('Reminder not found');
113 | }
114 |
115 | reminder.title = updateReminderDto.title
116 | ? updateReminderDto.title
117 | : reminder.title;
118 | reminder.content = updateReminderDto.content
119 | ? updateReminderDto.content
120 | : reminder.content;
121 | reminder.eventAt = updateReminderDto.eventAt
122 | ? updateReminderDto.eventAt
123 | : reminder.eventAt;
124 | reminder.alertOn = updateReminderDto.alertOn
125 | ? updateReminderDto.alertOn
126 | : reminder.alertOn;
127 | reminder.alarmAt = updateReminderDto.alarmAt
128 | ? updateReminderDto.alarmAt
129 | : reminder.alarmAt;
130 | reminder.situationId = updateReminderDto.situationId
131 | ? updateReminderDto.situationId
132 | : reminder.situationId;
133 |
134 | await this.reminderRepository.save(reminder);
135 |
136 | return new ReminderResponseDto(reminder);
137 | }
138 |
139 | async delete(id: number, user: User) {
140 | await this.reminderRepository.softDelete({
141 | id: id,
142 | user: {
143 | id: user.id,
144 | },
145 | });
146 | }
147 |
148 | async done(id: number, user: User) {
149 | await this.reminderRepository.update(
150 | {
151 | id: id,
152 | user: {
153 | id: user.id,
154 | },
155 | },
156 | { isDone: true },
157 | );
158 | return new ReminderStautsResponseDto(id, true);
159 | }
160 |
161 | async undone(id: number, user: User) {
162 | await this.reminderRepository.update(
163 | {
164 | id: id,
165 | user: {
166 | id: user.id,
167 | },
168 | },
169 | { isDone: false },
170 | );
171 | return new ReminderStautsResponseDto(id, false);
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/domain/reminder/test/reminder.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { User } from 'src/domain/users/entities/user.entity';
3 | import { Reminder } from '../entities/reminder.entity';
4 | import { ReminderController } from '../reminder.controller';
5 | import { ReminderService } from '../reminder.service';
6 | import { FindAllReminderQueryDto } from '../dto/requests/findAllReminder.request.dto';
7 | import { ReminderResponseDto } from '../dto/responses/reminder.response.dto';
8 |
9 | const mockRepository = () => ({});
10 |
11 | describe('ReminderController', () => {
12 | let reminderController: ReminderController;
13 | let reminderService: ReminderService;
14 | let reminder: Reminder;
15 | let user: User;
16 |
17 | beforeEach(async () => {
18 | const moduleRef = await Test.createTestingModule({
19 | controllers: [ReminderController],
20 | providers: [
21 | ReminderService,
22 | {
23 | provide: 'ReminderRepository',
24 | useFactory: mockRepository,
25 | },
26 | {
27 | provide: 'UserRepository',
28 | useFactory: mockRepository,
29 | },
30 | ],
31 | }).compile();
32 |
33 | reminderService = moduleRef.get(ReminderService);
34 | reminderController = moduleRef.get(ReminderController);
35 | reminder = new Reminder();
36 | user = new User();
37 | });
38 |
39 | describe('Test FindAll', () => {
40 | it('should return an array of reminder', async () => {
41 | const query = new FindAllReminderQueryDto();
42 | query.done = true;
43 | query.page = 1;
44 | query.take = 10;
45 |
46 | const result = {
47 | count: Number(),
48 | offset: Number(),
49 | limit: Number(),
50 | data: Reminder[10],
51 | };
52 | const user = new User();
53 | jest
54 | .spyOn(reminderService, 'findAll')
55 | .mockImplementation(() => Promise.resolve(result));
56 |
57 | expect(await reminderController.findAll(query, user)).toBe(result);
58 | });
59 | });
60 |
61 | describe('Test FindOne', () => {
62 | it('should return an array of reminder', async () => {
63 | const result = {
64 | data: new ReminderResponseDto(reminder),
65 | };
66 | jest
67 | .spyOn(reminderService, 'findOne')
68 | .mockImplementation(() =>
69 | Promise.resolve(new ReminderResponseDto(reminder)),
70 | );
71 |
72 | expect(await reminderController.findOne(1, user)).toStrictEqual(result);
73 | });
74 | });
75 |
76 | describe('Test Create', () => {
77 | it('should return reminder', async () => {
78 | const result = {
79 | data: Reminder,
80 | };
81 | jest
82 | .spyOn(reminderService, 'createReminder')
83 | .mockImplementation(() => Promise.resolve(reminder));
84 |
85 | expect(await reminderController.create(reminder, user)).toStrictEqual(
86 | result,
87 | );
88 | });
89 | });
90 |
91 | describe('Test Update', () => {
92 | it('should return reminder', async () => {
93 | const result = {
94 | data: Reminder,
95 | };
96 | jest
97 | .spyOn(reminderService, 'update')
98 | .mockImplementation(() => Promise.resolve(reminder));
99 |
100 | expect(await reminderController.update(1, reminder, user)).toStrictEqual(
101 | result,
102 | );
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/src/domain/reminder/test/reminder.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { User } from 'src/domain/users/entities/user.entity';
3 | import { Repository } from 'typeorm';
4 | import { Reminder } from '../entities/reminder.entity';
5 | import { ReminderService } from '../reminder.service';
6 | import { ReminderController } from '../reminder.controller';
7 | import { getRepositoryToken } from '@nestjs/typeorm';
8 | import { PaginationBuilder } from 'src/common/paginations/paginationBuilder.response';
9 | import { FindAllReminderQueryDto } from '../dto/requests/findAllReminder.request.dto';
10 | import { CreateReminderDto } from '../dto/requests/createReminder.request.dto';
11 | import { ReminderResponseDto } from '../dto/responses/reminder.response.dto';
12 | import { ReminderStautsResponseDto } from '../dto/responses/reminderStatus.response.dto';
13 | import { UpdateReminderDto } from '../dto/requests/updateReminder.request.dto';
14 | import { NotFoundException } from '@nestjs/common';
15 |
16 | const mockRepository = () => ({
17 | save: jest.fn(),
18 | findAndCount: jest.fn().mockResolvedValueOnce([[Reminder], 1]),
19 | findOne: jest.fn().mockResolvedValue(Reminder),
20 | update: jest.fn(),
21 | softDelete: jest.fn(),
22 | });
23 |
24 | type MockRepository = Partial, jest.Mock>>;
25 |
26 | describe('Reminder Service Test', () => {
27 | let reminderService: ReminderService;
28 | let reminderRepository: MockRepository;
29 | const user = new User();
30 |
31 | beforeEach(async () => {
32 | const moduleRef = await Test.createTestingModule({
33 | controllers: [ReminderController],
34 | providers: [
35 | ReminderService,
36 | {
37 | provide: 'ReminderRepository',
38 | useValue: mockRepository(),
39 | },
40 | ],
41 | }).compile();
42 |
43 | reminderService = moduleRef.get(ReminderService);
44 | reminderRepository = moduleRef.get(getRepositoryToken(Reminder));
45 | });
46 |
47 | describe('FindAll', () => {
48 | const findAllReminderQueryDto = new FindAllReminderQueryDto();
49 | findAllReminderQueryDto.page = 1;
50 | findAllReminderQueryDto.take = 10;
51 |
52 | const paginationResult = new PaginationBuilder()
53 | .setData([Reminder])
54 | .setTotalCount(1)
55 | .setPage(1)
56 | .setTake(10)
57 | .build();
58 |
59 | it('Find All User Reminders', () => {
60 | const result = reminderService.findAll(findAllReminderQueryDto, user);
61 | expect(result).resolves.toEqual(paginationResult);
62 | });
63 | it('Find All User Done Reminders', () => {
64 | findAllReminderQueryDto.done = true;
65 | const result = reminderService.findAll(findAllReminderQueryDto, user);
66 | expect(result).resolves.toEqual(paginationResult);
67 | });
68 | it('Find All User Not Done Reminders', () => {
69 | findAllReminderQueryDto.done = false;
70 | const result = reminderService.findAll(findAllReminderQueryDto, user);
71 | expect(result).resolves.toEqual(paginationResult);
72 | });
73 | it('Find All User Reminders When done is undefined', () => {
74 | findAllReminderQueryDto.done = undefined;
75 | const result = reminderService.findAll(findAllReminderQueryDto, user);
76 | expect(result).resolves.toEqual(paginationResult);
77 | });
78 | it('Find All User Reminders When done is null', () => {
79 | findAllReminderQueryDto.done = null;
80 | const result = reminderService.findAll(findAllReminderQueryDto, user);
81 | expect(result).resolves.toEqual(paginationResult);
82 | });
83 | });
84 |
85 | describe('FindOne', () => {
86 | const reminder = new Reminder();
87 | const reminderResponseDto = new ReminderResponseDto(reminder);
88 | const id = 1;
89 | it('Find One if exist', () => {
90 | const result = reminderService.findOne(id, user);
91 | expect(result).resolves.toEqual(reminderResponseDto);
92 | });
93 | it('Can not find reminder', () => {
94 | jest.spyOn(reminderRepository, 'findOne').mockResolvedValue(undefined);
95 | const result = reminderService.findOne(id, user);
96 | expect(result).rejects.toThrowError(
97 | new NotFoundException('Reminder not found'),
98 | );
99 | });
100 | it('Find reminder, but not users one', () => {
101 | expect(true).toBe(true);
102 | });
103 | });
104 |
105 | describe('Create Reminder', () => {
106 | const createReminder = new CreateReminderDto();
107 | const reminder = new Reminder();
108 | reminder.isDone = false;
109 | const reminderResult = new ReminderResponseDto(reminder);
110 | it('Create Reminder', () => {
111 | const result = reminderService.createReminder(createReminder, user);
112 | expect(result).resolves.toEqual(reminderResult);
113 | });
114 | });
115 |
116 | describe('Update Reminder', () => {
117 | const id = 1;
118 | const updateReminderDto = new UpdateReminderDto();
119 | const reminder = new Reminder();
120 | const reminderResult = new ReminderResponseDto(reminder);
121 | it('Can not find reminder', () => {
122 | jest.spyOn(reminderRepository, 'findOne').mockResolvedValue(undefined);
123 | const result = reminderService.update(id, updateReminderDto, user);
124 | expect(result).rejects.toThrowError(
125 | new NotFoundException('Reminder not found'),
126 | );
127 | });
128 | it('Update Reminder', () => {
129 | const result = reminderService.update(id, updateReminderDto, user);
130 | expect(result).resolves.toEqual(reminderResult);
131 | });
132 | });
133 |
134 | describe('Delete Reminder', () => {
135 | it('Can not find reminder', () => {
136 | expect(true).toBe(true);
137 | });
138 | it('Delete Reminder', () => {
139 | expect(true).toBe(true);
140 | });
141 | });
142 |
143 | describe('Change Reminder status to done', () => {
144 | const id = 1;
145 | const reminderStautsResponseDto = new ReminderStautsResponseDto(id, true);
146 | it('Can not find reminder', () => {
147 | // todo()
148 | });
149 | it('Delete Reminder', () => {
150 | const result = reminderService.done(id, user);
151 | expect(result).resolves.toEqual(reminderStautsResponseDto);
152 | });
153 | });
154 |
155 | describe('Change Reminder status to undone', () => {
156 | const id = 1;
157 | const reminderStautsResponseDto = new ReminderStautsResponseDto(id, false);
158 | it('Can not find reminder', () => {
159 | // todo()
160 | });
161 | it('Delete Reminder', () => {
162 | const result = reminderService.undone(id, user);
163 | expect(result).resolves.toEqual(reminderStautsResponseDto);
164 | });
165 | });
166 | });
167 |
--------------------------------------------------------------------------------
/src/domain/reply/dtos/requests/createReply.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsNumber, IsString } from 'class-validator';
3 |
4 | export class CreateReplyDto {
5 | @IsNumber()
6 | @ApiProperty({
7 | example: 1,
8 | description: '편지 id',
9 | })
10 | readonly letterBodyId: number;
11 |
12 | @IsString()
13 | @ApiProperty({
14 | example: '정말 고마워',
15 | description: '답장 내용',
16 | })
17 | readonly content: string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/domain/reply/entities/reply.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity('reply')
4 | export class Reply {
5 | @PrimaryGeneratedColumn('increment')
6 | id: number;
7 |
8 | @Column({ type: 'text', nullable: true })
9 | content: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/domain/reply/reply.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | HttpCode,
5 | HttpStatus,
6 | Post,
7 | UseGuards,
8 | } from '@nestjs/common';
9 | import { ReqUser } from 'src/common/decorators/user.decorators';
10 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
11 | import { User } from 'src/domain/users/entities/user.entity';
12 | import { CreateReplyDto } from './dtos/requests/createReply.request.dto';
13 | import { ReplyService } from './reply.service';
14 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
15 |
16 | @Controller('replies')
17 | @UseGuards(JwtAuthGuard)
18 | @ApiBearerAuth()
19 | @ApiTags('Reply API')
20 | export class ReplyController {
21 | constructor(private readonly replyService: ReplyService) {}
22 |
23 | @Post()
24 | @HttpCode(HttpStatus.CREATED)
25 | @ApiOperation({
26 | summary: '답장 생성 API',
27 | description: '다른 사용자의 글에 답장을 답니다.',
28 | })
29 | async create(@Body() replyDto: CreateReplyDto, @ReqUser() user: User) {
30 | return await this.replyService.createReply(replyDto, user);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/domain/reply/reply.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PassportModule } from '@nestjs/passport';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { LetterBody } from 'src/domain/letter/entities/letterBody.entity';
5 | import { Reply } from './entities/reply.entity';
6 | import { ReplyController } from './reply.controller';
7 | import { ReplyService } from './reply.service';
8 |
9 | @Module({
10 | imports: [TypeOrmModule.forFeature([Reply, LetterBody]), PassportModule],
11 | controllers: [ReplyController],
12 | providers: [ReplyService],
13 | })
14 | export class ReplyModule {}
15 |
--------------------------------------------------------------------------------
/src/domain/reply/reply.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConflictException,
3 | Injectable,
4 | NotFoundException,
5 | } from '@nestjs/common';
6 | import { InjectRepository } from '@nestjs/typeorm';
7 | import { LetterBody } from 'src/domain/letter/entities/letterBody.entity';
8 | import { User } from 'src/domain/users/entities/user.entity';
9 | import { Repository } from 'typeorm';
10 | import { CreateReplyDto } from './dtos/requests/createReply.request.dto';
11 | import { Reply } from './entities/reply.entity';
12 |
13 | @Injectable()
14 | export class ReplyService {
15 | constructor(
16 | @InjectRepository(Reply)
17 | private replyRepository: Repository,
18 | @InjectRepository(LetterBody)
19 | private letterBodyRepository: Repository,
20 | ) {}
21 |
22 | async createReply(replyDto: CreateReplyDto, user: User) {
23 | // TODO: 답장 쓸수 있는 편지인지 권한 확인 erd 부터 정립필요.
24 |
25 | const letterBody = await this.letterBodyRepository.findOne({
26 | where: { id: replyDto.letterBodyId },
27 | relations: ['reply'],
28 | });
29 |
30 | if (!letterBody)
31 | throw new NotFoundException(
32 | `LetterBody #${replyDto.letterBodyId} not found`,
33 | );
34 | if (letterBody.reply) throw new ConflictException(`Reply already exists`);
35 |
36 | const reply = new Reply();
37 | reply.content = replyDto.content;
38 |
39 | letterBody.reply = reply;
40 |
41 | return (await this.letterBodyRepository.save(letterBody)).reply;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/domain/sentence/dto/requests/createSentence.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsBoolean, IsNumber, IsString } from 'class-validator';
3 |
4 | export class CreateSentenceDto {
5 | @IsString()
6 | @ApiProperty({
7 | example: '생일 축하해',
8 | description: '문장 내용',
9 | })
10 | readonly content: string;
11 |
12 | @IsBoolean()
13 | @ApiProperty({
14 | example: true,
15 | description: '공유 여부',
16 | })
17 | readonly isShared: boolean;
18 |
19 | @IsNumber()
20 | @ApiProperty({
21 | example: 1,
22 | description: '문장 상황 id',
23 | })
24 | readonly situationId: number;
25 | }
26 |
--------------------------------------------------------------------------------
/src/domain/sentence/dto/responses/manysentence.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { SentenceResponseDto } from './sentence.response.dto';
3 |
4 | export class SituationSentenceResponseDto {
5 | @ApiProperty({
6 | type: [SentenceResponseDto],
7 | description: '사용자 추가 문장(최대 5개)을 배열로 반환합니다.',
8 | })
9 | userSentence: SentenceResponseDto[];
10 |
11 | @ApiProperty({
12 | type: [SentenceResponseDto],
13 | description: '가이드 문장(5개)을 배열로 반환합니다.',
14 | })
15 | guideSentence: SentenceResponseDto[];
16 | }
17 |
18 | export class AllSentenceResponseDto {
19 | @ApiProperty({
20 | type: [SentenceResponseDto],
21 | description: '사용자 추가 문장(최대 5개)을 배열로 반환합니다.',
22 | })
23 | userSentence: SentenceResponseDto[];
24 |
25 | @ApiProperty({
26 | type: [SentenceResponseDto],
27 | description: '가이드 문장(5개)을 배열로 반환합니다.',
28 | })
29 | guideSentence: SentenceResponseDto[];
30 | }
31 |
--------------------------------------------------------------------------------
/src/domain/sentence/dto/responses/sentence.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Sentence } from 'src/domain/sentence/entities/sentence.entity';
3 |
4 | export class SentenceResponseDto {
5 | @ApiProperty({
6 | example: 1,
7 | description: '문장 id',
8 | })
9 | id: number;
10 |
11 | @ApiProperty({
12 | example: '생일축하해~',
13 | description: '문장 내용',
14 | })
15 | content: string;
16 |
17 | @ApiProperty({
18 | example: 3,
19 | description: '상황 id',
20 | })
21 | situationId: number;
22 |
23 | @ApiProperty({
24 | example: 'user',
25 | description: '문장 타입(유저, 공통)',
26 | })
27 | type: string;
28 |
29 | @ApiProperty({
30 | example: true,
31 | description: '공유 여부',
32 | })
33 | isShared: boolean;
34 |
35 | @ApiProperty({
36 | example: '2021-01-01 00:00:00',
37 | description: '생성 일자',
38 | })
39 | createdAt: Date;
40 |
41 | constructor(sentence: Sentence) {
42 | this.id = sentence.id;
43 | this.situationId = sentence.situationId;
44 | this.content = sentence.content;
45 | this.type = sentence.type;
46 | this.isShared = sentence.isShared;
47 | this.createdAt = sentence.createdAt;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/domain/sentence/entities/sentence.entity.ts:
--------------------------------------------------------------------------------
1 | import { SentenceType } from 'src/domain/sentence/sentence.constant';
2 | import { User } from 'src/domain/users/entities/user.entity';
3 | import {
4 | Column,
5 | CreateDateColumn,
6 | Entity,
7 | JoinColumn,
8 | ManyToOne,
9 | PrimaryGeneratedColumn,
10 | UpdateDateColumn,
11 | } from 'typeorm';
12 |
13 | @Entity('sentence')
14 | export class Sentence {
15 | @PrimaryGeneratedColumn()
16 | id: number;
17 |
18 | @Column({ type: 'enum', enum: SentenceType, nullable: false })
19 | type: SentenceType;
20 |
21 | @ManyToOne(() => User)
22 | @JoinColumn({ name: 'userId', referencedColumnName: 'id' })
23 | user: User;
24 |
25 | @Column({ name: 'userId' })
26 | userId: number;
27 |
28 | @Column()
29 | content: string;
30 |
31 | @Column({ default: false })
32 | isShared: boolean;
33 |
34 | @Column({ nullable: true })
35 | myPreference: number;
36 |
37 | @Column({ nullable: true })
38 | totalPreference: number;
39 |
40 | @Column({ nullable: true })
41 | situationId: number;
42 |
43 | @CreateDateColumn()
44 | createdAt: Date;
45 |
46 | @UpdateDateColumn()
47 | updatedAt: Date;
48 | }
49 |
--------------------------------------------------------------------------------
/src/domain/sentence/sentence.constant.ts:
--------------------------------------------------------------------------------
1 | export enum SentenceType {
2 | USER = 'user',
3 | GUIDE = 'guide',
4 | }
5 |
--------------------------------------------------------------------------------
/src/domain/sentence/sentence.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | UseGuards,
11 | } from '@nestjs/common';
12 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
13 | import { CreateSentenceDto } from './dto/requests/createSentence.request.dto';
14 | import { SentenceService } from './sentence.service';
15 | import {
16 | ApiBearerAuth,
17 | ApiNotFoundResponse,
18 | ApiOperation,
19 | ApiResponse,
20 | ApiTags,
21 | } from '@nestjs/swagger';
22 | import { SentenceResponseDto } from './dto/responses/sentence.response.dto';
23 | import { ReqUser } from 'src/common/decorators/user.decorators';
24 | import { User } from 'src/domain/users/entities/user.entity';
25 | import { SituationSentenceResponseDto } from './dto/responses/manysentence.response.dto';
26 |
27 | @Controller('sentence')
28 | @ApiTags('Sentence API')
29 | @ApiBearerAuth()
30 | @UseGuards(JwtAuthGuard)
31 | export class SentenceController {
32 | constructor(private readonly sentenceService: SentenceService) {}
33 |
34 | @ApiOperation({
35 | summary: '상황별 문장 가져오기 API',
36 | description: '상황 id로 사용자 추가 문장, 가이드 문장을 가져옵니다.',
37 | })
38 | @ApiResponse({
39 | status: HttpStatus.OK,
40 | description: '상황 id로 사용자 추가 문장, 가이드 문장을 가져옵니다.',
41 | type: SituationSentenceResponseDto,
42 | })
43 | @Get('situation/:id')
44 | async findSentence(@ReqUser() user: User, @Param('id') situationId: number) {
45 | const userSentence = await this.sentenceService.findUserSentenceBySituation(
46 | user.id,
47 | situationId,
48 | );
49 | const guideSentence =
50 | await this.sentenceService.findGuideSentenceBySituation(situationId);
51 |
52 | return { data: { userSentence, guideSentence } };
53 | }
54 |
55 | @ApiOperation({
56 | summary: '문장 추가하기 API',
57 | description: '문장을 추가합니다.',
58 | })
59 | @ApiResponse({
60 | status: HttpStatus.CREATED,
61 | description: '문장이 추가되었습니다.',
62 | type: SentenceResponseDto,
63 | })
64 | @Post()
65 | @HttpCode(HttpStatus.CREATED)
66 | async createSentence(
67 | @ReqUser() user: User,
68 | @Body() sentenceDto: CreateSentenceDto,
69 | ) {
70 | const sentence = await this.sentenceService.createSentence(
71 | user,
72 | sentenceDto,
73 | );
74 | return { data: sentence };
75 | }
76 |
77 | @ApiOperation({
78 | summary: '내가 쓴 문장 가져오기 API',
79 | description: '내가 쓴 문장을 가져옵니다.',
80 | })
81 | @ApiResponse({
82 | status: HttpStatus.OK,
83 | description: '내가 쓴 문장을 문장 id로 가져옵니다',
84 | type: SentenceResponseDto,
85 | })
86 | @Get(':id')
87 | async findOneSentence(@ReqUser() user: User, @Param('id') id: number) {
88 | const sentence = await this.sentenceService.findOne(user, id);
89 | return { data: sentence };
90 | }
91 |
92 | @ApiOperation({
93 | summary: '문장 삭제하기 API',
94 | description: '문장을 삭제합니다.',
95 | })
96 | @ApiNotFoundResponse({ description: '문장을 찾을 수 없습니다.' })
97 | @ApiResponse({
98 | status: HttpStatus.NO_CONTENT,
99 | description: '문장이 삭제되었습니다.',
100 | })
101 | @Delete(':id')
102 | @HttpCode(HttpStatus.NO_CONTENT)
103 | async deleteSentence(@ReqUser() user: User, @Param('id') id: number) {
104 | return await this.sentenceService.deleteSentence(id, user);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/domain/sentence/sentence.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PassportModule } from '@nestjs/passport';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { AuthModule } from 'src/auth/auth.module';
5 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
6 | import { User } from 'src/domain/users/entities/user.entity';
7 | import { Sentence } from './entities/sentence.entity';
8 | import { SentenceController } from './sentence.controller';
9 | import { SentenceService } from './sentence.service';
10 |
11 | @Module({
12 | imports: [
13 | TypeOrmModule.forFeature([Sentence, User]),
14 | PassportModule,
15 | AuthModule,
16 | ],
17 | controllers: [SentenceController],
18 | providers: [SentenceService, JwtStrategy],
19 | })
20 | export class SentenceModule {}
21 |
--------------------------------------------------------------------------------
/src/domain/sentence/sentence.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { SentenceType } from 'src/domain/sentence/sentence.constant';
4 | import { User } from 'src/domain/users/entities/user.entity';
5 | import { Repository } from 'typeorm';
6 | import { CreateSentenceDto } from './dto/requests/createSentence.request.dto';
7 | import { SentenceResponseDto } from './dto/responses/sentence.response.dto';
8 | import { Sentence } from './entities/sentence.entity';
9 |
10 | @Injectable()
11 | export class SentenceService {
12 | constructor(
13 | @InjectRepository(Sentence)
14 | private sentenceRepository: Repository,
15 | ) {}
16 |
17 | async findOne(user: User, id: number): Promise {
18 | const sentence = await this.sentenceRepository.findOne({
19 | where: { id: id, userId: user.id },
20 | });
21 |
22 | if (!sentence) {
23 | throw new NotFoundException({
24 | type: 'NOT_FOUND',
25 | message: `Sentence #${id} not found`,
26 | });
27 | }
28 |
29 | return new SentenceResponseDto(sentence);
30 | }
31 |
32 | async findUserSentenceBySituation(userId: number, situationId: number) {
33 | const sentenceList = await this.sentenceRepository
34 | .createQueryBuilder('sentence')
35 | .where('sentence.userId = :id', { id: userId })
36 | .andWhere('sentence.type = :type', { type: SentenceType.USER })
37 | .andWhere('sentence.situationId = :situationId', {
38 | situationId: situationId,
39 | })
40 | .take(5)
41 | .getMany();
42 | return sentenceList.map((sentence) => {
43 | return new SentenceResponseDto(sentence);
44 | });
45 | }
46 |
47 | async findGuideSentenceBySituation(situationId: number) {
48 | const sentenceList = await this.sentenceRepository
49 | .createQueryBuilder('sentence')
50 | .andWhere('sentence.type = :type', { type: SentenceType.GUIDE })
51 | .andWhere('sentence.situationId = :situationId', {
52 | situationId: situationId,
53 | })
54 | .orderBy('rand ()')
55 | .take(5)
56 | .getMany();
57 | return sentenceList.map((sentence) => {
58 | return new SentenceResponseDto(sentence);
59 | });
60 | }
61 |
62 | async createSentence(
63 | user: User,
64 | sentenceDto: CreateSentenceDto,
65 | ): Promise {
66 | const newSentence = new Sentence();
67 | newSentence.type = SentenceType.USER;
68 | newSentence.userId = user.id;
69 | newSentence.situationId = sentenceDto.situationId;
70 | newSentence.isShared = sentenceDto.isShared;
71 | newSentence.content = sentenceDto.content;
72 |
73 | const result = await this.sentenceRepository.save(newSentence);
74 |
75 | return new SentenceResponseDto(result);
76 | }
77 |
78 | async deleteSentence(id: number, user: User): Promise {
79 | const deleted = await this.sentenceRepository.delete({
80 | id,
81 | userId: user.id,
82 | });
83 |
84 | if (!deleted.affected)
85 | throw new NotFoundException({
86 | type: 'NOT_FOUND',
87 | message: `Sentence #${id} not found`,
88 | });
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/domain/sentence/test/sentence.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SentenceController } from '../sentence.controller';
3 |
4 | describe('SentenceController', () => {
5 | let controller: SentenceController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [SentenceController],
10 | }).compile();
11 |
12 | controller = module.get(SentenceController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/domain/sentence/test/sentence.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { SentenceService } from '../sentence.service';
3 | import { Sentence } from '../entities/sentence.entity';
4 | import { Repository } from 'typeorm';
5 | import { SentenceController } from '../sentence.controller';
6 | import { User } from 'src/domain/users/entities/user.entity';
7 | import { getRepositoryToken } from '@nestjs/typeorm';
8 | import { SentenceResponseDto } from '../dto/responses/sentence.response.dto';
9 | import { NotFoundException } from '@nestjs/common';
10 | import { CreateSentenceDto } from '../dto/requests/createSentence.request.dto';
11 | import { SentenceType } from 'src/domain/sentence/sentence.constant';
12 |
13 | const mockRepository = () => ({
14 | save: jest.fn(),
15 | findAndCount: jest.fn().mockResolvedValueOnce([[Sentence], 1]),
16 | findOne: jest.fn().mockResolvedValue(Sentence),
17 | update: jest.fn(),
18 | softDelete: jest.fn(),
19 | delete: jest.fn(),
20 | });
21 |
22 | type MockRepository = Partial, jest.Mock>>;
23 |
24 | describe('SentenceService', () => {
25 | let sentenceService: SentenceService;
26 | let sentenceRepository: MockRepository;
27 | const user = new User();
28 | user.id = 1;
29 |
30 | beforeEach(async () => {
31 | const moduleRef = await Test.createTestingModule({
32 | controllers: [SentenceController],
33 | providers: [
34 | SentenceService,
35 | {
36 | provide: 'SentenceRepository',
37 | useValue: mockRepository(),
38 | },
39 | ],
40 | }).compile();
41 |
42 | sentenceService = moduleRef.get(SentenceService);
43 | sentenceRepository = moduleRef.get(getRepositoryToken(Sentence));
44 | });
45 |
46 | describe('FindOne', () => {
47 | const sentence = new Sentence();
48 | const sentenceResponseDto = new SentenceResponseDto(sentence);
49 | const id = 1;
50 | it('Find One if exist', () => {
51 | const result = sentenceService.findOne(user, id);
52 | expect(result).resolves.toEqual(sentenceResponseDto);
53 | });
54 | it('Can not find sentence', () => {
55 | jest.spyOn(sentenceRepository, 'findOne').mockResolvedValue(undefined);
56 | const result = sentenceService.findOne(user, id);
57 | expect(result).rejects.toThrowError(
58 | new NotFoundException({
59 | type: 'NOT_FOUND',
60 | message: `Sentence #1 not found`,
61 | }),
62 | );
63 | });
64 | });
65 |
66 | describe('create', () => {
67 | const sentenceDto: CreateSentenceDto = {
68 | content: 'test',
69 | isShared: false,
70 | situationId: 1,
71 | };
72 | const sentence: Sentence = {
73 | id: 1,
74 | situationId: 1,
75 | type: SentenceType.USER,
76 | content: 'test',
77 | userId: 1,
78 | isShared: false,
79 | myPreference: 0,
80 | totalPreference: 0,
81 | createdAt: new Date(),
82 | updatedAt: new Date(),
83 | user: user,
84 | };
85 | const sentenceResponseDto = new SentenceResponseDto(sentence);
86 | it('Create Sentence', () => {
87 | jest.spyOn(sentenceRepository, 'save').mockResolvedValue(sentence);
88 | const result = sentenceService.createSentence(user, sentenceDto);
89 | expect(result).resolves.toEqual(sentenceResponseDto);
90 | });
91 | });
92 |
93 | describe('delete Sentence', () => {
94 | const id = 1;
95 | it('Delete Sentence success', () => {
96 | const deleteResult: { affected: number } = { affected: 1 };
97 | jest.spyOn(sentenceRepository, 'delete').mockResolvedValue(deleteResult);
98 | sentenceService.deleteSentence(id, user);
99 | });
100 | it("Can't find sentence", () => {
101 | const deleteResult: { affected: number } = { affected: 0 };
102 | jest.spyOn(sentenceRepository, 'delete').mockResolvedValue(deleteResult);
103 | const result = sentenceService.deleteSentence(id, user);
104 | expect(result).rejects.toThrowError(
105 | new NotFoundException({
106 | type: 'NOT_FOUND',
107 | message: `Sentence #1 not found`,
108 | }),
109 | );
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/domain/users/dto/requests/updateUser.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateKakaoUserDto } from 'src/auth/dto/requests/createKakaoUser.dto';
3 |
4 | export class UpdateUserDto extends PartialType(CreateKakaoUserDto) {}
5 |
--------------------------------------------------------------------------------
/src/domain/users/dto/response/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | // noinspection HttpUrlsUsage
4 | export class UserResponseDto {
5 | @ApiProperty({
6 | example: 1,
7 | description: '유저 id',
8 | })
9 | id: number;
10 |
11 | @ApiProperty({
12 | example: '김꼬깃',
13 | description: '유저 이름',
14 | })
15 | name: string;
16 |
17 | @ApiProperty({
18 | example:
19 | 'http://k.kakaocdn.net/dn/kbBXv/btrThGICmvf/DgD0mvr0IKvCKNkl4oNAI1/img_640x640.jp',
20 | description: '카톡 프로필 이미지 url',
21 | })
22 | profileImg: string;
23 |
24 | @ApiProperty({
25 | example: 'true',
26 | description: '꼬깃 메모 알림 설정 여부',
27 | })
28 | remindOn: boolean;
29 |
30 | @ApiProperty({
31 | example: 'true',
32 | description: '알림 설정 여부부',
33 | })
34 | alertOn: boolean;
35 |
36 | @ApiProperty({
37 | example: 'false',
38 | description: '웰컴 팝업 보여졌는지',
39 | })
40 | welcomePopupView: boolean;
41 |
42 | @ApiProperty({
43 | example: 'true',
44 | description:
45 | '친구 목록 가져오기 항목 동의했는지, 안했으면 /auth/friends로 추가항목 동의 후 가져올수 있습니다',
46 | })
47 | allowFriendsList: boolean;
48 |
49 | constructor(user: any) {
50 | this.id = user.id;
51 | this.name = user.name;
52 | this.profileImg = user.profileImg;
53 | this.remindOn = user.userInfo.remindOn;
54 | this.alertOn = user.userInfo.alertOn;
55 | this.welcomePopupView = user.userInfo.welcomePopupView;
56 | this.allowFriendsList = user.social.allowFriendsList;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/domain/users/entities/social.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity({ name: 'social' })
4 | export class Social {
5 | @PrimaryGeneratedColumn()
6 | id: number;
7 |
8 | @Column()
9 | clientId: string;
10 |
11 | @Column({ default: false })
12 | allowFriendsList: boolean;
13 | }
14 |
--------------------------------------------------------------------------------
/src/domain/users/entities/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | DeleteDateColumn,
5 | Entity,
6 | JoinColumn,
7 | OneToOne,
8 | PrimaryGeneratedColumn,
9 | UpdateDateColumn,
10 | } from 'typeorm';
11 | import { Social } from './social.entity';
12 | import { UserInfo } from './userInfo.entity';
13 |
14 | @Entity({ name: 'user' })
15 | export class User {
16 | @PrimaryGeneratedColumn()
17 | id: number;
18 |
19 | @Column()
20 | name: string;
21 |
22 | @Column()
23 | nickname: string;
24 |
25 | @Column()
26 | profileImg: string;
27 |
28 | @CreateDateColumn()
29 | createdAt: Date;
30 |
31 | @UpdateDateColumn()
32 | updatedAt: Date;
33 |
34 | @DeleteDateColumn()
35 | deletedAt: Date;
36 |
37 | @OneToOne(() => Social, { cascade: true })
38 | @JoinColumn({ name: 'socialId', referencedColumnName: 'id' })
39 | social: Social;
40 |
41 | @OneToOne(() => UserInfo, { cascade: true })
42 | @JoinColumn({ name: 'userInfoId', referencedColumnName: 'id' })
43 | userInfo: UserInfo;
44 | }
45 |
--------------------------------------------------------------------------------
/src/domain/users/entities/userInfo.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | Entity,
5 | PrimaryGeneratedColumn,
6 | UpdateDateColumn,
7 | } from 'typeorm';
8 |
9 | @Entity({ name: 'userInfo' })
10 | export class UserInfo {
11 | @PrimaryGeneratedColumn()
12 | id: number;
13 |
14 | @Column({ nullable: true })
15 | birthday: string;
16 |
17 | @Column({ nullable: true })
18 | email: string;
19 |
20 | @Column({ nullable: true })
21 | gender: string;
22 |
23 | @Column({ default: true })
24 | remindOn: boolean;
25 |
26 | @Column({ default: true })
27 | alertOn: boolean;
28 |
29 | @Column({ default: false })
30 | welcomePopupView: boolean;
31 |
32 | @CreateDateColumn()
33 | createdAt: Date;
34 |
35 | @UpdateDateColumn()
36 | updatedAt: Date;
37 | }
38 |
--------------------------------------------------------------------------------
/src/domain/users/users.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersController } from './users.controller';
3 | import { UsersService } from './users.service';
4 |
5 | describe('UsersController', () => {
6 | let controller: UsersController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [UsersController],
11 | providers: [UsersService],
12 | }).compile();
13 |
14 | controller = module.get(UsersController);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(controller).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/domain/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | HttpStatus,
5 | Param,
6 | Patch,
7 | UseGuards,
8 | } from '@nestjs/common';
9 | import { UsersService } from './users.service';
10 | import { JwtAuthGuard } from 'src/common/guards/jwtAuth.guard';
11 | import { AuthService } from 'src/auth/auth.service';
12 | import { ReqUser } from 'src/common/decorators/user.decorators';
13 | import {
14 | ApiBearerAuth,
15 | ApiOperation,
16 | ApiResponse,
17 | ApiTags,
18 | } from '@nestjs/swagger';
19 | import { User } from './entities/user.entity';
20 | import { UserResponseDto } from './dto/response/user.response.dto';
21 |
22 | @Controller('users')
23 | @UseGuards(JwtAuthGuard)
24 | @ApiBearerAuth()
25 | @ApiTags('Users API')
26 | export class UsersController {
27 | constructor(
28 | private readonly usersService: UsersService,
29 | private readonly authService: AuthService,
30 | ) {}
31 |
32 | @ApiOperation({
33 | summary: '유저 목록 가져오기 API',
34 | description: '유저 목록을 가져옵니다.',
35 | })
36 | @ApiResponse({
37 | status: HttpStatus.OK,
38 | description: '서비스에 가입된 모든 유저를 가져옵니다',
39 | type: [User],
40 | })
41 | @Get()
42 | async findAll() {
43 | const users = await this.usersService.findAll();
44 | return { data: users };
45 | }
46 |
47 | @ApiOperation({
48 | summary: '내 정보 조회 API',
49 | description: '내 정보를 가져옵니다.',
50 | })
51 | @ApiResponse({
52 | status: HttpStatus.OK,
53 | description: '내 정보를 반환합니다.',
54 | type: UserResponseDto,
55 | })
56 | @Get('/me')
57 | async findMe(@ReqUser() user: User) {
58 | const me = await this.usersService.findUserByMe(user);
59 | return { data: me };
60 | }
61 |
62 | @ApiOperation({
63 | summary: '유저 정보 수정 API',
64 | description: '유저 정보를 수정합니다.',
65 | })
66 | @Patch(':id')
67 | async update(@Param('id') id: string) {
68 | const user = await this.usersService.update(+id);
69 | return { data: user };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/domain/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UsersService } from './users.service';
3 | import { UsersController } from './users.controller';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { Social } from './entities/social.entity';
6 | import { User } from './entities/user.entity';
7 | import { UserInfo } from './entities/userInfo.entity';
8 | import { AuthModule } from 'src/auth/auth.module';
9 | import { JwtStrategy } from 'src/auth/strategy/jwt.strategy';
10 | import { PassportModule } from '@nestjs/passport';
11 |
12 | @Module({
13 | imports: [
14 | TypeOrmModule.forFeature([User, UserInfo, Social]),
15 | PassportModule,
16 | AuthModule,
17 | ],
18 | controllers: [UsersController],
19 | providers: [UsersService, JwtStrategy],
20 | })
21 | export class UsersModule {}
22 |
--------------------------------------------------------------------------------
/src/domain/users/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersService } from './users.service';
3 |
4 | describe('UsersService', () => {
5 | let service: UsersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UsersService],
10 | }).compile();
11 |
12 | service = module.get(UsersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/domain/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { UserResponseDto } from './dto/response/user.response.dto';
5 | import { User } from './entities/user.entity';
6 |
7 | @Injectable()
8 | export class UsersService {
9 | constructor(
10 | @InjectRepository(User) private userRepository: Repository,
11 | ) {}
12 |
13 | async findUserByMe(user: User) {
14 | const userInfo = await this.userRepository.findOne({
15 | where: { id: user.id },
16 | relations: { userInfo: true, social: true },
17 | select: ['id', 'name', 'profileImg', 'social', 'userInfo'],
18 | });
19 | return new UserResponseDto(userInfo);
20 | }
21 |
22 | async update(id: number) {
23 | const user = await this.userRepository.findOneBy({ id });
24 |
25 | if (!user) {
26 | throw new NotFoundException({
27 | statusCode: 404,
28 | message: 'This User is not available',
29 | error: 'Bad Request to this Id, This User is not available',
30 | });
31 | }
32 |
33 | // TODO : add update user
34 | return await this.userRepository.save(user);
35 | }
36 |
37 | async findAll() {
38 | return await this.userRepository.find();
39 | }
40 |
41 | async findUserBySocialId(socialId: number): Promise {
42 | const socialUser = await this.userRepository
43 | .createQueryBuilder('user')
44 | .leftJoinAndSelect('user.social', 'social')
45 | .where('social.clientId = :clientId', { clientId: socialId })
46 | .getOne();
47 |
48 | return socialUser;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/infrastructor/S3/s3.multer.ts:
--------------------------------------------------------------------------------
1 | import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
2 | import multerS3 from 'multer-s3';
3 | import { S3Client } from '@aws-sdk/client-s3';
4 | import path from 'path';
5 | import { ConfigService } from '@nestjs/config';
6 |
7 | export const multerAttachedImgOptionsFactory = (
8 | configService: ConfigService,
9 | ): MulterOptions => {
10 | const s3 = new S3Client({
11 | region: configService.get('AWS_BUCKET_REGION'),
12 | credentials: {
13 | accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
14 | secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
15 | },
16 | });
17 |
18 | return {
19 | storage: multerS3({
20 | s3,
21 | bucket: configService.get('AWS_BUCKET_NAME'),
22 | key(_req, file, done) {
23 | const ext = path.extname(file.originalname);
24 | const basename = path.basename(file.originalname, ext);
25 | const timestamp = new Date().valueOf();
26 | done(null, `attachedimg/${basename}_${timestamp}${ext}`);
27 | },
28 | }),
29 | limits: { fileSize: 10 * 1024 * 1024 },
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/src/infrastructor/document/document.swagger.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
3 |
4 | export function setupSwagger(app: INestApplication): void {
5 | const options = new DocumentBuilder()
6 | .setTitle('ggo-geet API Docs')
7 | .setDescription('ggo-geet API description')
8 | .setVersion('1.0.0')
9 | .addBearerAuth({
10 | type: 'http',
11 | scheme: 'bearer',
12 | name: 'JWT',
13 | in: 'header',
14 | })
15 | .build();
16 |
17 | const document = SwaggerModule.createDocument(app, options);
18 | SwaggerModule.setup('api-docs', app, document);
19 | }
20 |
--------------------------------------------------------------------------------
/src/infrastructor/monitoring/monitoring.telegram.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { Injectable } from '@nestjs/common';
3 |
4 | @Injectable()
5 | export class TelegramMonitoringService {
6 | private readonly chatId: string;
7 | private readonly botToken: string;
8 |
9 | constructor() {
10 | this.chatId = process.env.TELEGRAM_CHAT_ID;
11 | this.botToken = process.env.TELEGRAM_BOT_TOKEN;
12 | }
13 |
14 | public sendAlert(message: string) {
15 | const config = {
16 | method: 'get',
17 | url: `https://api.telegram.org/bot${this.botToken}/sendMessage?chat_id=${this.chatId}&text=${message}`,
18 | headers: {},
19 | };
20 |
21 | axios(config)
22 | .then(function (response) {
23 | console.log(JSON.stringify(response.data));
24 | })
25 | .catch(function (error) {
26 | console.log(error);
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ValidationPipe } from '@nestjs/common';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { setupSwagger } from './infrastructor/document/document.swagger';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | app.enableCors({
9 | origin: function (origin, callback) {
10 | callback(null, true);
11 | },
12 | });
13 | app.useGlobalPipes(
14 | new ValidationPipe({
15 | whitelist: true,
16 | forbidNonWhitelisted: true,
17 | transform: true,
18 | }),
19 | );
20 | setupSwagger(app);
21 | await app.listen(3000);
22 | }
23 |
24 | bootstrap();
25 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from './../src/app.module';
4 | import { INestApplication } from '@nestjs/common';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeAll(async () => {
10 | const moduleFixture = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "esModuleInterop": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------