Skip to content

Commit

Permalink
v1.1.0
Browse files Browse the repository at this point in the history
v1.1.0
  • Loading branch information
char-yb authored Sep 13, 2024
2 parents 56fdc51 + 8622750 commit 8ed851b
Show file tree
Hide file tree
Showing 28 changed files with 419 additions and 103 deletions.
148 changes: 148 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<div align=center>

<img src="./images/introduction.png" alt="NotFound">


## 세상 모든 반려동물을 한 자리에서! 왈왈🐶

<b>반려동물과 일상의 추억을 기억하고 싶으신 적 없으신가요? <br/>
왈왈은 반려동물과 함께할 수 있는 소소한 미션을 통해 추억을 기록하고, 다른 반려동물의 일상을 발견할 수 있는
펫 커뮤니티 서비스입니다 </b>

<br/>

<a href="https://apps.apple.com/kr/app/%EC%99%88%EC%99%88/id6553981069" target="_blank">
<img src="https://img.shields.io/badge/AppStore-0D96F6.svg?style=flat&logo=app-store&logoColor=white" alt="App Store"/>
</a>
<a href="https://www.instagram.com/walwal._.official/" target="_blank">
<img src="http://img.shields.io/badge/Instagram-E4405F?style=flat&logo=instagram&logoColor=white" alt="Instagram"/>
</a>
<br>
<a href="https://yapp-workspace.notion.site/5-8a385156703047aabf1e3706f4753cc6?pvs=4" target="_blank">
<img src="http://img.shields.io/badge/서비스_소개-%23000000?style=flat&logo=notion&logoColor=white" alt="서비스 소개"/>
</a>
<a href="https://medium.com/@olderstonebed" target="_blank">
<img src="http://img.shields.io/badge/개발_로그-12100E?style=flat&logo=medium&logoColor=white" alt="개발 로그"/>
</a>
<a href="https://dis.qa/e6J" target="_blank">
<img src="http://img.shields.io/badge/메이커_로그-0000FF?style=flat&logo=Pinboard&logoColor=white" alt="메이커 로그"/>
</a>

</div>

<br/>

## ✨ IA(Information Architecture)

<img src="./images/IA.png">

<br>

---

## 📌 Package Architecture
왈왈 서버 패키지 아키텍처는 레이어드 아키텍처로 구성하였습니다. <br>
위 소개한 IA에서 큰 규모의 기능이 정의되지 않아, 당장은 클린 아키텍처 또는 헥사고날 아키텍처의 도입이 필요하지 않다고 판단하였습니다.
<br>

```
// 프로젝트 전체 구조
├── src.main.java.com.depromeet.stonebed
│ ├── domain
│ ├── global
│ ├── infra
// domain 패키지는 비즈니스 로직을 담당하며 레이어드 아키텍처 구성
│ ├── domain
│ └── auth
│ └── common
│ └── fcm
│ └── feed
│ └── follow
│ └── image
│ └── member
│ └── mission
│ └── missionHistory
│ └── missionRecordBoost
│ └── sqs
// global 패키지는 전역 설정 담당
│ ├── global
│ └── annotation
│ └── config
│ └── common
│ └── error
│ └── filter
│ └── interceptor
│ └── security
│ └── util
// infra는 외부 연동 및 클라우드 구성 담당
│ ├── infra
│ └── config
│ └── properties
// ...
```

<br>

---

## 💻 Tech Stack
<img src="./images/tech-stack.png">

<br>

---

## 🏛️ System Architecture
<img src="./images/cloud-architecture.png">

### 📦 CI/CD
- Github Actions
- Docker Hub
- Docker compose

---

## 🖥️ Monitoring
모니터링은 Prometheus를 사용하여 서버의 상태에 대한 메트릭 수집과, Grafana를 사용하여 대시보드를 구성하였고, <br/>
Loki, Promtail을 사용하여 로그 수집 및 저장을 구성하였습니다. <br />
또한, MySQL에 대한 SlowQuery 발생 시 Slack Webhook을 통한 알림 전송을 Lambda 함수로 작성하여 구성하였습니다.<br/>


---

<div align="center">

<h2> 🧑‍💻 Server Developer </h2>
<div style="display: inline-block;">

<table>
<tr>
<th>차윤범</th>
<th>노관옥</th>
<th>박윤찬</th>
</tr>
<tr>
<td><a href="https://github.com/char-yb"><img style="border-radius: 20%;" src="https://avatars.githubusercontent.com/u/68099546?v=4" width=100px alt="_" /></a></td>
<td><a href="https://github.com/kwanok"><img src="https://avatars.githubusercontent.com/u/61671343?v=4" width=100px alt="_" /></a></td>
<td><a href="https://github.com/dbscks97"><img style="border-radius: 20%;" src="https://avatars.githubusercontent.com/u/75676309?v=4" width=100px alt="_" /></a></td>
</tr>
<tr>
<td><strong>Server</strong> (Leader)</td>
<td><strong>Server</strong></td>
<td><strong>Server</strong></td>
</tr>
</table>

</div>

</div>


<div align=center>
<img src="./images/app_qr.png" width="570">
</div>

###
Binary file added images/IA.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/app_qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/cloud-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/introduction.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/tech-stack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ private void validateMemberStatusDelete(MemberStatus status) {
private Member registerMember(Member member, CreateMemberRequest request) {
member.updateProfile(
Profile.createProfile(
request.nickname(), member.getProfile().getProfileImageUrl()));
request.nickname(),
request.profileImageUrl() == null
? member.getProfile().getProfileImageUrl()
: request.profileImageUrl()));
member.updateRaisePet(request.raisePet());
member.updateMemberRole(MemberRole.USER);
memberRepository.save(member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.depromeet.stonebed.domain.fcm.domain.FcmToken;
import com.depromeet.stonebed.domain.fcm.dto.response.FcmNotificationDto;
import com.depromeet.stonebed.domain.fcm.dto.response.FcmNotificationResponse;
import com.depromeet.stonebed.domain.member.dao.MemberRepository;
import com.depromeet.stonebed.domain.member.domain.Member;
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository;
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository;
Expand All @@ -23,6 +24,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
Expand All @@ -41,17 +43,27 @@ public class FcmNotificationService {
private final MissionRecordBoostRepository missionRecordBoostRepository;
private final MissionRecordRepository missionRecordRepository;
private final FcmRepository fcmRepository;
private final MemberRepository memberRepository;
private final MemberUtil memberUtil;

private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

private static final long POPULAR_THRESHOLD = 500;
private static final long FIRST_BOOST_THRESHOLD = 1;
private static final long POPULAR_THRESHOLD = 1000;
private static final long SUPER_POPULAR_THRESHOLD = 5000;

public void saveNotification(
FcmNotificationType type, String title, String message, Long targetId, Boolean isRead) {
final Member member = memberUtil.getCurrentMember();
FcmNotificationType type,
String title,
String message,
Long targetId,
Long memberId,
Boolean isRead) {
Member member =
memberRepository
.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

FcmNotification notification =
FcmNotification.create(type, title, message, member, targetId, isRead);
Expand Down Expand Up @@ -132,22 +144,38 @@ public void checkAndSendBoostNotification(MissionRecord missionRecord) {
missionRecordBoostRepository.sumBoostCountByMissionRecord(missionRecord.getId());

if (totalBoostCount != null) {
FcmNotificationConstants notificationConstants =
Optional<FcmNotificationConstants> notificationType =
determineNotificationType(totalBoostCount);

if (notificationConstants != null) {
sendBoostNotification(missionRecord, notificationConstants);
}
notificationType.ifPresent(
type -> {
if (!notificationAlreadySent(missionRecord, type)) {
sendBoostNotification(missionRecord, type);
}
});
}
}

private FcmNotificationConstants determineNotificationType(Long totalBoostCount) {
if (totalBoostCount == POPULAR_THRESHOLD) {
return FcmNotificationConstants.POPULAR;
} else if (totalBoostCount == SUPER_POPULAR_THRESHOLD) {
return FcmNotificationConstants.SUPER_POPULAR;
private boolean notificationAlreadySent(
MissionRecord missionRecord, FcmNotificationConstants notificationConstants) {
return notificationRepository.existsByTargetIdAndTypeAndTitle(
missionRecord.getId(),
FcmNotificationType.BOOSTER,
notificationConstants.getTitle());
}

private Optional<FcmNotificationConstants> determineNotificationType(Long totalBoostCount) {
if (totalBoostCount >= SUPER_POPULAR_THRESHOLD) {
return Optional.of(FcmNotificationConstants.SUPER_POPULAR);
}
return null;
if (totalBoostCount >= POPULAR_THRESHOLD) {
return Optional.of(FcmNotificationConstants.POPULAR);
}
if (totalBoostCount >= FIRST_BOOST_THRESHOLD) {
return Optional.of(FcmNotificationConstants.FIRST_BOOST);
}

return Optional.empty();
}

private void sendBoostNotification(
Expand All @@ -158,18 +186,24 @@ private void sendBoostNotification(
.map(FcmToken::getToken)
.orElseThrow(() -> new CustomException(ErrorCode.FAILED_TO_FIND_FCM_TOKEN));

String deepLink =
FcmNotification.generateDeepLink(
FcmNotificationType.BOOSTER, missionRecord.getId());

FcmMessage fcmMessage =
FcmMessage.of(
notificationConstants.getTitle(),
notificationConstants.getMessage(),
token);
token,
deepLink);
sqsMessageService.sendMessage(fcmMessage);

saveNotification(
FcmNotificationType.BOOSTER,
notificationConstants.getTitle(),
notificationConstants.getMessage(),
missionRecord.getId(),
missionRecord.getMember().getId(),
false);
}

Expand Down Expand Up @@ -208,8 +242,10 @@ private List<FcmNotification> buildNotificationList(
public void sendAndNotifications(String title, String message, List<String> tokens) {
List<List<String>> batches = createBatches(tokens, 10);

String deepLink = FcmNotification.generateDeepLink(FcmNotificationType.MISSION, null);

for (List<String> batch : batches) {
sqsMessageService.sendBatchMessages(batch, title, message);
sqsMessageService.sendBatchMessages(batch, title, message, deepLink);
}

List<FcmNotification> notifications = buildNotificationList(title, message, tokens);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
Expand Down Expand Up @@ -63,20 +63,17 @@ private List<String> getIncompleteMissionTokens() {
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
LocalDateTime endOfDay = startOfDay.plusDays(1);

return missionRecordRepository
.findAllByCreatedAtBetweenAndStatusNot(
startOfDay, endOfDay, MissionRecordStatus.COMPLETED)
.stream()
.map(
missionRecord -> {
FcmToken fcmToken =
fcmRepository
.findByMemberAndMemberStatus(
missionRecord.getMember(), MemberStatus.NORMAL)
.orElse(null);
return fcmToken != null ? fcmToken.getToken() : null;
})
.filter(Objects::nonNull)
.toList();
List<Long> completedMemberIds =
missionRecordRepository
.findAllByCreatedAtBetweenAndStatus(
startOfDay, endOfDay, MissionRecordStatus.COMPLETED)
.stream()
.map(missionRecord -> missionRecord.getMember().getId())
.collect(Collectors.toList());

return fcmRepository.findAllByMemberStatus(MemberStatus.NORMAL).stream()
.filter(fcmToken -> !completedMemberIds.contains(fcmToken.getMember().getId()))
.map(FcmToken::getToken)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.depromeet.stonebed.domain.fcm.dao;

import com.depromeet.stonebed.domain.fcm.domain.FcmNotification;
import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType;
import com.depromeet.stonebed.domain.member.domain.Member;
import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -21,6 +22,8 @@ List<FcmNotification> findByMemberIdAndCreatedAtLessThanEqual(

boolean existsByCreatedAtLessThan(LocalDateTime createdAt);

boolean existsByTargetIdAndTypeAndTitle(Long targetId, FcmNotificationType type, String title);

// Delete
@Modifying
@Query("DELETE FROM FcmNotification fn WHERE fn.member.id = :memberId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface FcmRepository extends JpaRepository<FcmToken, Long>, FcmReposit

List<FcmToken> findAll();

List<FcmToken> findAllByUpdatedAtBefore(LocalDateTime cutoffDate);
List<FcmToken> findAllByMemberStatus(MemberStatus status);

Optional<FcmToken> findByMemberAndMemberStatus(Member member, MemberStatus status);
List<FcmToken> findAllByUpdatedAtBefore(LocalDateTime cutoffDate);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.depromeet.stonebed.domain.fcm.domain;

public record FcmMessage(String title, String body, String token) {
public record FcmMessage(String title, String body, String token, String deepLink) {

public static FcmMessage of(String title, String body, String token) {
return new FcmMessage(title, body, token);
public static FcmMessage of(String title, String body, String token, String deepLink) {
return new FcmMessage(title, body, token, deepLink);
}
}
Loading

0 comments on commit 8ed851b

Please sign in to comment.