diff --git a/README.assets/architecture.png b/README.assets/architecture.png new file mode 100644 index 00000000..31619c9a Binary files /dev/null and b/README.assets/architecture.png differ diff --git a/README.assets/erd.png b/README.assets/erd.png new file mode 100644 index 00000000..5301dfbb Binary files /dev/null and b/README.assets/erd.png differ diff --git a/README.assets/ggumtle.png b/README.assets/ggumtle.png new file mode 100644 index 00000000..bb1fec7f Binary files /dev/null and b/README.assets/ggumtle.png differ diff --git a/README.assets/ggumtle1.png b/README.assets/ggumtle1.png new file mode 100644 index 00000000..83decc9d Binary files /dev/null and b/README.assets/ggumtle1.png differ diff --git a/README.assets/ggumtle2.png b/README.assets/ggumtle2.png new file mode 100644 index 00000000..72afc838 Binary files /dev/null and b/README.assets/ggumtle2.png differ diff --git a/README.assets/ggumtle3.png b/README.assets/ggumtle3.png new file mode 100644 index 00000000..6fcf02f5 Binary files /dev/null and b/README.assets/ggumtle3.png differ diff --git a/README.assets/ggumtle4.png b/README.assets/ggumtle4.png new file mode 100644 index 00000000..d207d0aa Binary files /dev/null and b/README.assets/ggumtle4.png differ diff --git a/README.assets/ggumtle5.png b/README.assets/ggumtle5.png new file mode 100644 index 00000000..19dc374c Binary files /dev/null and b/README.assets/ggumtle5.png differ diff --git a/README.assets/ggumtle6.png b/README.assets/ggumtle6.png new file mode 100644 index 00000000..d693e1af Binary files /dev/null and b/README.assets/ggumtle6.png differ diff --git a/README.assets/ggumtle7.png b/README.assets/ggumtle7.png new file mode 100644 index 00000000..fbcc2906 Binary files /dev/null and b/README.assets/ggumtle7.png differ diff --git a/README.assets/title.gif b/README.assets/title.gif new file mode 100644 index 00000000..c5b444b0 Binary files /dev/null and b/README.assets/title.gif differ diff --git a/README.md b/README.md index 9413b2e7..0f681116 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ -# ggumtle -아자아자! 잘하자! \ No newline at end of file +# 꿈 : 틀 소개 +![Alt text](/README.assets/title.gif) + + 꿈, 이제는 정말 이뤄나갈 시간. + 마음에만 담아두지 말고 꿈틀에 담아보세요. + + 다른 사람들은 가슴 한켠에 어떤 꿈을 품은 채 살아갈까요? + 나와 같은 꿈을 먼저 이룩한 선배 꿈꾼이는 어떤 여정을 겪었을까요? + + 꿈틀에서 다채롭고 감동적인 꿈들을 만나보세요. + 꿈나라로 초대합니다. 꿈틀에서 만나요. +
+ +# 서비스 주요 기능 + +#### 1. 친밀도에 따른 레이다 조회 및 꿈 카테고리 별 레이다 조회 +#### 2. 설정주기에 따른 꿈 리마인드 +#### 3. 유저간 상호작용(팔로우, 댓글, 리액션) +#### 4. 검색 기능으로 사용자, 꿈틀, 후기 목록 조회 +#### 5. 필터 기능을 적용한 유저 타임라인 조회 +#### 6. SSE를 활용한 상호작용 알람 +#### 7. 꿈 달성시 타임 캡슐 확인 및 소셜 공유 기능 + +
+ +# 서비스 시연 동영상 + +
+ +# 개발환경 + +### Backend + + + + + + + + +### Frontend + + + + + + + + + + + +### CI/CD + + + + +### 협업 툴 + + + + + +
+ +# 서비스 아키텍쳐 +![Alt text](/README.assets/architecture.png) +
+ +# ER Diagram +![Alt text](/README.assets/erd.png) + +
+ +# 팀 소개 +|이우성|이원주|전지환|서준호|신창엽|조인화| +|:---:|:---:|:---:|:---:|:---:|:---:| +||||||| +|`FE` `QA`
[@leewooseong](https://github.com/leewooseong)|`FE 리더`
[@3o14](https://github.com/3o14)|`BE` `PM`
[@DarkBlackRice](https://github.com/DarkBlackRice)|`BE` `INFRA`
[@ho97s](https://github.com/ho97s)|`BE 리더`
[@404](https://github.com/404-not-foundl)|`BE` `QA`
[@jjjoina](https://github.com/jjjoina)| +
+ diff --git a/backend/src/main/java/com/ggums/ggumtle/common/handler/AlarmHandler.java b/backend/src/main/java/com/ggums/ggumtle/common/handler/AlarmHandler.java index c346f315..77d27f68 100644 --- a/backend/src/main/java/com/ggums/ggumtle/common/handler/AlarmHandler.java +++ b/backend/src/main/java/com/ggums/ggumtle/common/handler/AlarmHandler.java @@ -2,10 +2,12 @@ import com.ggums.ggumtle.common.exception.CustomException; import com.ggums.ggumtle.common.exception.ExceptionType; +import com.ggums.ggumtle.dto.response.model.AlarmDto; import com.ggums.ggumtle.entity.*; import com.ggums.ggumtle.repository.AlarmRepository; import com.ggums.ggumtle.repository.BucketRepository; import com.ggums.ggumtle.repository.UserRepository; +import com.ggums.ggumtle.service.AlarmService; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; @@ -32,7 +34,7 @@ public class AlarmHandler { public final Map userEmitters = new ConcurrentHashMap<>(); private final AlarmRepository alarmRepository; private final BucketRepository bucketRepository; - private final UserRepository userRepository; + private final AlarmService alarmService; // Giving connection signals to client @Async @@ -61,14 +63,14 @@ public void createUserAlarm(User receiver, User sender, AlarmType type){ if (!receiver.getAlarm()) { return; } - Alarm alarm = Alarm.builder() + Alarm alarm = Alarm.builder() .receiver(receiver) .sender(sender) .type(type) .dataId(sender != null ? sender.getId() : null) .build(); alarmRepository.save(alarm); - sendEventToUser(receiver.getId()); + sendEventToUser(receiver.getId(), alarm); } /** @@ -91,7 +93,7 @@ public void createBucketAlarm(User receiver, User sender, AlarmType type, Bucket .dataId(bucket.getId()) .build(); alarmRepository.save(alarm); - sendEventToUser(receiver.getId()); + sendEventToUser(receiver.getId(), alarm); } /** @@ -114,7 +116,7 @@ public void createReviewAlarm(User receiver, User sender, AlarmType type, Review .dataId(review.getId()) .build(); alarmRepository.save(alarm); - sendEventToUser(receiver.getId()); + sendEventToUser(receiver.getId(), alarm); } /** @@ -147,7 +149,7 @@ public void createCommentAlarm(User receiver, User sender, AlarmType type, Strin } alarmRepository.save(alarm); - sendEventToUser(receiver.getId()); + sendEventToUser(receiver.getId(), alarm); } /** @@ -164,7 +166,7 @@ public void createReminderAlarm(User receiver, Bucket bucket){ .dataId(bucket.getId()) .build(); alarmRepository.save(alarm); - sendEventToUser(receiver.getId()); + sendEventToUser(receiver.getId(), alarm); } @Scheduled(cron = "0 0 12 * * ?") @@ -198,11 +200,11 @@ private boolean shouldSendReminder(Bucket bucket, LocalDate today) { } @Async - protected void sendEventToUser(Long userId) { + protected void sendEventToUser(Long userId, Alarm alarm) { SseEmitter emitter = userEmitters.get(userId); if (emitter != null) { try { - emitter.send(SseEmitter.event().name("serverEvent").data("readAlarm")); + emitter.send(SseEmitter.event().name("serverEvent").data(alarmService.convertToAlarmResponseDto(alarm))); } catch (IOException e) { emitter.completeWithError(e); userEmitters.remove(userId); diff --git a/backend/src/main/java/com/ggums/ggumtle/controller/AlarmController.java b/backend/src/main/java/com/ggums/ggumtle/controller/AlarmController.java index e27467f7..aaf02507 100644 --- a/backend/src/main/java/com/ggums/ggumtle/controller/AlarmController.java +++ b/backend/src/main/java/com/ggums/ggumtle/controller/AlarmController.java @@ -127,4 +127,14 @@ public Response readAllAlarm(@AuthenticationPrincipal User user){ return new Response("message", alarmService.readAllAlarm(user)); } + @PostMapping("/send-reminder") + @Operation(summary = "리마인더 알람 발송", description = "리마인더 알람 전송") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "sent") + }) + public Response sendReminder(){ + alarmHandler.remindBucketAlarm(); + return new Response("message", "sent"); + } + } diff --git a/backend/src/main/java/com/ggums/ggumtle/dto/response/AlarmResponseDto.java b/backend/src/main/java/com/ggums/ggumtle/dto/response/AlarmResponseDto.java index 69e6d1c7..3a8eb30c 100644 --- a/backend/src/main/java/com/ggums/ggumtle/dto/response/AlarmResponseDto.java +++ b/backend/src/main/java/com/ggums/ggumtle/dto/response/AlarmResponseDto.java @@ -1,7 +1,6 @@ package com.ggums.ggumtle.dto.response; -import com.ggums.ggumtle.dto.response.model.AlarmListDto; -import com.ggums.ggumtle.entity.AlarmType; +import com.ggums.ggumtle.dto.response.model.AlarmDto; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -10,5 +9,5 @@ @Getter @Setter @Builder public class AlarmResponseDto { - private Page alarmList; + private Page alarmList; } diff --git a/backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmListDto.java b/backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmDto.java similarity index 97% rename from backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmListDto.java rename to backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmDto.java index 9aedf315..b7fb4ccd 100644 --- a/backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmListDto.java +++ b/backend/src/main/java/com/ggums/ggumtle/dto/response/model/AlarmDto.java @@ -10,7 +10,7 @@ @Getter @Setter @Builder -public class AlarmListDto { +public class AlarmDto { @Schema(description = "알람 id", example = "1") private Long alarmId; diff --git a/backend/src/main/java/com/ggums/ggumtle/service/AlarmService.java b/backend/src/main/java/com/ggums/ggumtle/service/AlarmService.java index 060d3e2b..eb7e66b6 100644 --- a/backend/src/main/java/com/ggums/ggumtle/service/AlarmService.java +++ b/backend/src/main/java/com/ggums/ggumtle/service/AlarmService.java @@ -3,9 +3,7 @@ import com.ggums.ggumtle.common.exception.CustomException; import com.ggums.ggumtle.common.exception.ExceptionType; import com.ggums.ggumtle.dto.response.AlarmResponseDto; -import com.ggums.ggumtle.dto.response.BucketSearchResponseDto; -import com.ggums.ggumtle.dto.response.model.AlarmListDto; -import com.ggums.ggumtle.dto.response.model.BucketSearchListDto; +import com.ggums.ggumtle.dto.response.model.AlarmDto; import com.ggums.ggumtle.entity.Alarm; import com.ggums.ggumtle.entity.User; import com.ggums.ggumtle.repository.AlarmRepository; @@ -13,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,12 +50,14 @@ public String alarmRead(User user, Long alarmId){ // getting list of alarm public AlarmResponseDto alarmList(User user, Pageable pageable){ - Page alarms = alarmRepository.findByReceiver(user, pageable); - Page alarmList = alarms.map(this::convertToAlarmResponseDto); + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdDate").descending()); + + Page alarms = alarmRepository.findByReceiver(user, sortedPageable); + Page alarmList = alarms.map(this::convertToAlarmResponseDto); return AlarmResponseDto.builder().alarmList(alarmList).build(); } - private AlarmListDto convertToAlarmResponseDto(Alarm alarm){ + public AlarmDto convertToAlarmResponseDto(Alarm alarm){ String timeUnit; long time; @@ -79,7 +81,7 @@ private AlarmListDto convertToAlarmResponseDto(Alarm alarm){ time = ChronoUnit.MINUTES.between(alarm.getCreatedDate(), LocalDateTime.now()); } - return AlarmListDto.builder() + return AlarmDto.builder() .alarmId(alarm.getId()) .sender(alarm.getSender() != null ? alarm.getSender().getUserNickname() : null) .senderProfileImage(alarm.getSender() != null ? alarm.getSender().getUserProfileImage() : null) diff --git "a/exec/1. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" "b/exec/1-2. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" similarity index 71% rename from "exec/1. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" rename to "exec/1-2. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" index 72264233..a109f98a 100644 Binary files "a/exec/1. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" and "b/exec/1-2. Gitlab \354\206\214\354\212\244 \355\201\264\353\241\240 \354\235\264\355\233\204 \353\271\214\353\223\234 \353\260\217 \353\260\260\355\217\254 \352\264\200\353\240\250 \353\254\270\354\204\234.pdf" differ diff --git "a/exec/3. DB \353\215\244\355\224\204 \355\214\214\354\235\274.zip" "b/exec/3. DB \353\215\244\355\224\204 \355\214\214\354\235\274.zip" new file mode 100644 index 00000000..4aecceb4 Binary files /dev/null and "b/exec/3. DB \353\215\244\355\224\204 \355\214\214\354\235\274.zip" differ diff --git a/frontend/dev-dist/sw.js b/frontend/dev-dist/sw.js index 8d4f8fc3..b0950571 100644 --- a/frontend/dev-dist/sw.js +++ b/frontend/dev-dist/sw.js @@ -13,80 +13,87 @@ // If the loader is already loaded, just stop. if (!self.define) { - let registry = {}; + let registry = {} - // Used for `eval` and `importScripts` where we can't get script URL by other means. - // In both cases, it's safe to use a global var because those functions are synchronous. - let nextDefineUri; + // Used for `eval` and `importScripts` where we can't get script URL by other means. + // In both cases, it's safe to use a global var because those functions are synchronous. + let nextDefineUri - const singleRequire = (uri, parentUri) => { - uri = new URL(uri + ".js", parentUri).href; - return registry[uri] || ( - - new Promise(resolve => { - if ("document" in self) { - const script = document.createElement("script"); - script.src = uri; - script.onload = resolve; - document.head.appendChild(script); - } else { - nextDefineUri = uri; - importScripts(uri); - resolve(); - } - }) - - .then(() => { - let promise = registry[uri]; - if (!promise) { - throw new Error(`Module ${uri} didn’t register its module`); - } - return promise; - }) - ); - }; + const singleRequire = (uri, parentUri) => { + uri = new URL(uri + '.js', parentUri).href + return ( + registry[uri] || + new Promise((resolve) => { + if ('document' in self) { + const script = document.createElement('script') + script.src = uri + script.onload = resolve + document.head.appendChild(script) + } else { + nextDefineUri = uri + importScripts(uri) + resolve() + } + }).then(() => { + let promise = registry[uri] + if (!promise) { + throw new Error(`Module ${uri} didn’t register its module`) + } + return promise + }) + ) + } - self.define = (depsNames, factory) => { - const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; - if (registry[uri]) { - // Module is already loading or loaded. - return; - } - let exports = {}; - const require = depUri => singleRequire(depUri, uri); - const specialDeps = { - module: { uri }, - exports, - require - }; - registry[uri] = Promise.all(depsNames.map( - depName => specialDeps[depName] || require(depName) - )).then(deps => { - factory(...deps); - return exports; - }); - }; + self.define = (depsNames, factory) => { + const uri = + nextDefineUri || ('document' in self ? document.currentScript.src : '') || location.href + if (registry[uri]) { + // Module is already loading or loaded. + return + } + let exports = {} + const require = (depUri) => singleRequire(depUri, uri) + const specialDeps = { + module: { uri }, + exports, + require, + } + registry[uri] = Promise.all( + depsNames.map((depName) => specialDeps[depName] || require(depName)) + ).then((deps) => { + factory(...deps) + return exports + }) + } } -define(['./workbox-b5f7729d'], (function (workbox) { 'use strict'; +define(['./workbox-b5f7729d'], function (workbox) { + 'use strict' - self.skipWaiting(); - workbox.clientsClaim(); + self.skipWaiting() + workbox.clientsClaim() - /** - * The precacheAndRoute() method efficiently caches and responds to - * requests for URLs in the manifest. - * See https://goo.gl/S9QRab - */ - workbox.precacheAndRoute([{ - "url": "registerSW.js", - "revision": "3ca0b8505b4bec776b69afdba2768812" - }, { - "url": "index.html", - "revision": "0.vlken83r8o8" - }], {}); - workbox.cleanupOutdatedCaches(); - workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { - allowlist: [/^\/$/] - })); - -})); + /** + * The precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ + workbox.precacheAndRoute( + [ + { + url: 'registerSW.js', + revision: '3ca0b8505b4bec776b69afdba2768812', + }, + { + url: 'index.html', + revision: '0.fptplrrmgcg', + }, + ], + {} + ) + workbox.cleanupOutdatedCaches() + workbox.registerRoute( + new workbox.NavigationRoute(workbox.createHandlerBoundToURL('index.html'), { + allowlist: [/^\/$/], + }) + ) +}) diff --git a/frontend/index.html b/frontend/index.html index cc6a8b8e..7f0a21ce 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,7 +9,8 @@ - + +