From dc70c25b160e8985b21b30af6426c506960c5b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=AC=EC=9E=AC=EC=97=BD?= Date: Mon, 9 Jun 2025 14:14:25 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=95=8C=EB=B0=94=EC=83=9D?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AC=EC=9E=A5=EB=8B=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=B4=EA=B3=A0=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9E=A5=EB=8B=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20(#81)=20-=20FCM=20Token=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B2=98=EB=A6=AC=20-=20Notifica?= =?UTF-8?q?tionEntity=20targetToken=20=ED=95=84=EB=93=9C=20nullable?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workreport/StaffWorkReportFacade.java | 9 +++++++ .../notification/NotificationService.java | 25 ++++++++++++++++--- .../notification/NotificationEntity.java | 2 +- .../NotificationJpaRepository.java | 9 ++++--- .../notification/NotificationType.java | 2 +- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/mangoboss/app/api/facade/workreport/StaffWorkReportFacade.java b/app/src/main/java/com/mangoboss/app/api/facade/workreport/StaffWorkReportFacade.java index 4b6f92fa..bd26dcef 100644 --- a/app/src/main/java/com/mangoboss/app/api/facade/workreport/StaffWorkReportFacade.java +++ b/app/src/main/java/com/mangoboss/app/api/facade/workreport/StaffWorkReportFacade.java @@ -1,6 +1,7 @@ package com.mangoboss.app.api.facade.workreport; import com.mangoboss.app.common.util.S3PreSignedUrlManager; +import com.mangoboss.app.domain.service.notification.NotificationService; import com.mangoboss.app.domain.service.staff.StaffService; import com.mangoboss.app.domain.service.workreport.WorkReportService; import com.mangoboss.app.dto.s3.response.UploadPreSignedUrlResponse; @@ -9,6 +10,7 @@ import com.mangoboss.storage.metadata.S3FileType; import com.mangoboss.storage.staff.StaffEntity; import com.mangoboss.storage.workreport.WorkReportEntity; +import com.mangoboss.storage.workreport.WorkReportTargetType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,11 +23,18 @@ public class StaffWorkReportFacade { private final WorkReportService workReportService; private final StaffService staffService; + private final NotificationService notificationService; private final S3PreSignedUrlManager s3PreSignedUrlManager; public WorkReportResponse createWorkReport(final Long storeId, final Long userId, final WorkReportCreateRequest request) { StaffEntity staff = staffService.getVerifiedStaff(userId, storeId); final WorkReportEntity entity = workReportService.createWorkReport(storeId, staff.getId(), request.content(), request.reportImageUrl(), request.targetType()); + + if (request.targetType() == WorkReportTargetType.TO_BOSS) { + final Long bossUserId = staff.getStore().getBoss().getId(); + notificationService.saveWorkReportNotificationToBoss(bossUserId, storeId, staff.getUser().getName()); + } + return WorkReportResponse.fromEntity(entity, staff); } diff --git a/app/src/main/java/com/mangoboss/app/domain/service/notification/NotificationService.java b/app/src/main/java/com/mangoboss/app/domain/service/notification/NotificationService.java index bc6885d3..5ba22785 100644 --- a/app/src/main/java/com/mangoboss/app/domain/service/notification/NotificationService.java +++ b/app/src/main/java/com/mangoboss/app/domain/service/notification/NotificationService.java @@ -28,11 +28,15 @@ private void saveNotification(final Long userId, final Long storeId, final Strin final String content, final NotificationType type, final String path) { final String clickUrl = frontendUrl + path; final List tokens = deviceTokenRepository.findActiveTokensByUserId(userId); - for (String token : tokens) { - final NotificationEntity notification = NotificationEntity.create( - userId, storeId, title, content, null, clickUrl, type, token - ); + + if (tokens.isEmpty()) { + final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, null); notificationRepository.save(notification); + } else { + for (String token : tokens) { + final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, token); + notificationRepository.save(notification); + } } } @@ -159,4 +163,17 @@ public void saveAttendanceEditRejectNotification(final AttendanceEditEntity atte public List getNotificationsByUserAndStore(final Long userId, final Long storeId) { return notificationRepository.findByUserIdAndStoreIdOrderByCreatedAtDesc(userId, storeId); } + + @Transactional + public void saveWorkReportNotificationToBoss(final Long bossUserId, final Long storeId, final String staffName) { + String content = String.format("%s님이 보고사항을 작성했어요.", staffName); + saveNotification( + bossUserId, + storeId, + "보고사항 작성", + content, + NotificationType.WORK_REPORT, + "/boss/task?type=report" + ); + } } diff --git a/storage/src/main/java/com/mangoboss/storage/notification/NotificationEntity.java b/storage/src/main/java/com/mangoboss/storage/notification/NotificationEntity.java index 054360da..77526136 100644 --- a/storage/src/main/java/com/mangoboss/storage/notification/NotificationEntity.java +++ b/storage/src/main/java/com/mangoboss/storage/notification/NotificationEntity.java @@ -23,7 +23,7 @@ public class NotificationEntity extends BaseTimeEntity { private Long storeId; - @Column(nullable = false, length = 5000) + @Column(length = 5000) private String targetToken; @Column(nullable = false) diff --git a/storage/src/main/java/com/mangoboss/storage/notification/NotificationJpaRepository.java b/storage/src/main/java/com/mangoboss/storage/notification/NotificationJpaRepository.java index 173e6aea..e2dadedd 100644 --- a/storage/src/main/java/com/mangoboss/storage/notification/NotificationJpaRepository.java +++ b/storage/src/main/java/com/mangoboss/storage/notification/NotificationJpaRepository.java @@ -11,10 +11,11 @@ public interface NotificationJpaRepository extends JpaRepository { @Query(""" - SELECT n FROM NotificationEntity n - WHERE - n.sendStatus IN :sendStatuses - AND n.retryCount < :maxRetry + SELECT n FROM NotificationEntity n + WHERE + n.sendStatus IN :sendStatuses + AND n.retryCount < :maxRetry + AND n.targetToken IS NOT NULL """) List findSendableNotifications( @Param("sendStatuses") List sendStatuses, diff --git a/storage/src/main/java/com/mangoboss/storage/notification/NotificationType.java b/storage/src/main/java/com/mangoboss/storage/notification/NotificationType.java index ca6fe1ba..38f5254e 100644 --- a/storage/src/main/java/com/mangoboss/storage/notification/NotificationType.java +++ b/storage/src/main/java/com/mangoboss/storage/notification/NotificationType.java @@ -1,5 +1,5 @@ package com.mangoboss.storage.notification; public enum NotificationType { - CONTRACT, SUBSTITUTE, SCHEDULE + CONTRACT, SUBSTITUTE, SCHEDULE, WORK_REPORT } \ No newline at end of file From c92d3585dc3b296a30b3e2a23c40e1ff04fb5916 Mon Sep 17 00:00:00 2001 From: hyunzzii Date: Mon, 9 Jun 2025 14:53:28 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EA=B2=B0=EA=B7=BC=20=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AutoClockOutService.java | 13 ++-- .../NotificationAutoClockOutService.java | 61 +++++++++++++++++++ .../persistence/ScheduleRepositoryImpl.java | 7 +-- .../common/repository/ScheduleRepository.java | 7 +-- .../domain/service/LateClockInService.java | 12 ++-- ...va => NotificationLateClockInService.java} | 16 +++-- .../schedule/ScheduleJpaRepository.java | 21 ++++--- ...=> ScheduleForNotificationProjection.java} | 5 +- 8 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/NotificationAutoClockOutService.java rename batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/{NotificationForLateClockInService.java => NotificationLateClockInService.java} (73%) rename storage/src/main/java/com/mangoboss/storage/schedule/projection/{ScheduleForLateClockInProjection.java => ScheduleForNotificationProjection.java} (79%) diff --git a/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/AutoClockOutService.java b/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/AutoClockOutService.java index 48f0632e..28db989f 100644 --- a/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/AutoClockOutService.java +++ b/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/AutoClockOutService.java @@ -6,11 +6,11 @@ import com.mangoboss.storage.attendance.ClockInStatus; import com.mangoboss.storage.attendance.ClockOutStatus; import com.mangoboss.storage.schedule.ScheduleEntity; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.Clock; import java.time.LocalDateTime; import java.util.List; @@ -20,12 +20,13 @@ public class AutoClockOutService { private final AttendanceRepository attendanceRepository; private final ScheduleRepository scheduleRepository; - private final Clock clock; + private final NotificationAutoClockOutService notificationAutoClockOutService; @Transactional public void autoClockOut() { - List schedules = scheduleRepository.findAllSchedulesWithoutClockOut(); - List attendances = schedules.stream().map(schedule -> { + List schedules = scheduleRepository.findAllSchedulesWithoutClockOut(); + List attendances = schedules.stream().map(projection -> { + ScheduleEntity schedule = projection.getSchedule(); if (schedule.getAttendance() == null) { return recordAbsentAttendance(schedule); } @@ -41,4 +42,8 @@ private AttendanceEntity recordAbsentAttendance(final ScheduleEntity schedule) { private AttendanceEntity recordNormalClockOutAttendance(final AttendanceEntity attendance, final LocalDateTime clockOutTime) { return attendance.recordClockOut(clockOutTime, ClockOutStatus.NORMAL); } + + private void notifyAbsentClockOut(final List projections) { + notificationAutoClockOutService.saveNotifications(projections); + } } diff --git a/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/NotificationAutoClockOutService.java b/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/NotificationAutoClockOutService.java new file mode 100644 index 00000000..576ecebb --- /dev/null +++ b/batch/src/main/java/com/mangoboss/batch/auto_clock_out/domain/service/NotificationAutoClockOutService.java @@ -0,0 +1,61 @@ +package com.mangoboss.batch.auto_clock_out.domain.service; + +import com.mangoboss.batch.common.repository.DeviceTokenRepository; +import com.mangoboss.batch.common.repository.NotificationRepository; +import com.mangoboss.storage.notification.NotificationEntity; +import com.mangoboss.storage.notification.NotificationType; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class NotificationAutoClockOutService { + private final NotificationRepository notificationRepository; + private final DeviceTokenRepository deviceTokenRepository; + + @Value("${frontend-url}") + private String frontendUrl; + + + private List generateNotification(final Long userId, final Long storeId, final String title, + final String content, final NotificationType type, final String path) { + String clickUrl = frontendUrl + path; + List tokens = deviceTokenRepository.findActiveTokensByUserId(userId); + if (tokens.isEmpty()) { + final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, null); + return List.of(notification); + } + return tokens.stream() + .map(token -> NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, token)) + .toList(); + } + + private List generateAbsentClockInNotification(final Long userId, final Long storeId, final String staffName) { + String content = String.format("%s님이 결근처리 되었어요.", staffName); + return generateNotification( + userId, + storeId, + "결근 알림", + content, + NotificationType.SCHEDULE, + "/boss/schedule" + ); + } + + @Transactional + public void saveNotifications(final List projections) { + List notifications = projections.stream() + .flatMap(schedule -> generateAbsentClockInNotification( + schedule.getBossId(), + schedule.getStoreId(), + schedule.getStaffName() + ).stream()) + .toList(); + notificationRepository.saveAll(notifications); + } +} diff --git a/batch/src/main/java/com/mangoboss/batch/common/persistence/ScheduleRepositoryImpl.java b/batch/src/main/java/com/mangoboss/batch/common/persistence/ScheduleRepositoryImpl.java index 3f2405ec..326d31c4 100644 --- a/batch/src/main/java/com/mangoboss/batch/common/persistence/ScheduleRepositoryImpl.java +++ b/batch/src/main/java/com/mangoboss/batch/common/persistence/ScheduleRepositoryImpl.java @@ -1,9 +1,8 @@ package com.mangoboss.batch.common.persistence; import com.mangoboss.batch.common.repository.ScheduleRepository; -import com.mangoboss.storage.schedule.ScheduleEntity; import com.mangoboss.storage.schedule.ScheduleJpaRepository; -import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -18,13 +17,13 @@ public class ScheduleRepositoryImpl implements ScheduleRepository { private final Clock clock; @Override - public List findAllSchedulesWithoutClockOut() { + public List findAllSchedulesWithoutClockOut() { LocalDateTime oneHourAgo = LocalDateTime.now(clock).minusHours(1); return scheduleJpaRepository.findAllSchedulesWithoutClockOut(oneHourAgo); } @Override - public List findAllSchedulesWithoutClockIn() { + public List findAllSchedulesWithoutClockIn() { LocalDateTime temMinuteAgo = LocalDateTime.now(clock).minusMinutes(10); return scheduleJpaRepository.findLateSchedulesWithoutAlarm(temMinuteAgo); } diff --git a/batch/src/main/java/com/mangoboss/batch/common/repository/ScheduleRepository.java b/batch/src/main/java/com/mangoboss/batch/common/repository/ScheduleRepository.java index c5cc8817..4e4d8d8c 100644 --- a/batch/src/main/java/com/mangoboss/batch/common/repository/ScheduleRepository.java +++ b/batch/src/main/java/com/mangoboss/batch/common/repository/ScheduleRepository.java @@ -1,12 +1,11 @@ package com.mangoboss.batch.common.repository; -import com.mangoboss.storage.schedule.ScheduleEntity; -import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import java.util.List; public interface ScheduleRepository { - List findAllSchedulesWithoutClockOut(); + List findAllSchedulesWithoutClockOut(); - List findAllSchedulesWithoutClockIn(); + List findAllSchedulesWithoutClockIn(); } diff --git a/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/LateClockInService.java b/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/LateClockInService.java index 50046756..3d300ff9 100644 --- a/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/LateClockInService.java +++ b/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/LateClockInService.java @@ -1,7 +1,7 @@ package com.mangoboss.batch.late_clock_in.domain.service; import com.mangoboss.batch.common.repository.ScheduleRepository; -import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,15 +12,15 @@ @RequiredArgsConstructor public class LateClockInService { private final ScheduleRepository scheduleRepository; - private final NotificationForLateClockInService notificationService; + private final NotificationLateClockInService notificationService; @Transactional - public void notifyLateClockIn(){ - List lateSchedules = scheduleRepository.findAllSchedulesWithoutClockIn(); - if (lateSchedules.isEmpty()) { + public void notifyLateClockIn() { + List projections = scheduleRepository.findAllSchedulesWithoutClockIn(); + if (projections.isEmpty()) { return; } - notificationService.saveNotifications(lateSchedules); + notificationService.saveNotifications(projections); } } diff --git a/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationForLateClockInService.java b/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationLateClockInService.java similarity index 73% rename from batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationForLateClockInService.java rename to batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationLateClockInService.java index 0bc3cf9b..f3898a7b 100644 --- a/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationForLateClockInService.java +++ b/batch/src/main/java/com/mangoboss/batch/late_clock_in/domain/service/NotificationLateClockInService.java @@ -4,7 +4,7 @@ import com.mangoboss.batch.common.repository.NotificationRepository; import com.mangoboss.storage.notification.NotificationEntity; import com.mangoboss.storage.notification.NotificationType; -import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -14,7 +14,7 @@ @Service @RequiredArgsConstructor -public class NotificationForLateClockInService { +public class NotificationLateClockInService { private final NotificationRepository notificationRepository; private final DeviceTokenRepository deviceTokenRepository; @@ -25,12 +25,16 @@ private List generateNotification(final Long userId, final L final String content, final NotificationType type, final String path) { String clickUrl = frontendUrl + path; List tokens = deviceTokenRepository.findActiveTokensByUserId(userId); + if (tokens.isEmpty()) { + final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, null); + return List.of(notification); + } return tokens.stream() .map(token -> NotificationEntity.createWithMetaId(userId, storeId, title, content, null, clickUrl, type, token, scheduleId)) .toList(); } - private List generateContractSignNotification(final Long userId, final Long storeId, final String staffName, final Long scheduleId) { + private List generateLateClockInNotification(final Long userId, final Long storeId, final String staffName, final Long scheduleId) { String content = String.format("%s님이 출근 시간으로부터 10분이 지나도 출근하지 않았어요.", staffName); return generateNotification( userId, @@ -44,9 +48,9 @@ private List generateContractSignNotification(final Long use } @Transactional - public void saveNotifications(final List scheduleProjections) { - List notifications = scheduleProjections.stream() - .flatMap(schedule -> generateContractSignNotification( + public void saveNotifications(final List projections) { + List notifications = projections.stream() + .flatMap(schedule -> generateLateClockInNotification( schedule.getBossId(), schedule.getStoreId(), schedule.getStaffName(), diff --git a/storage/src/main/java/com/mangoboss/storage/schedule/ScheduleJpaRepository.java b/storage/src/main/java/com/mangoboss/storage/schedule/ScheduleJpaRepository.java index 5c04fc90..3f147d53 100644 --- a/storage/src/main/java/com/mangoboss/storage/schedule/ScheduleJpaRepository.java +++ b/storage/src/main/java/com/mangoboss/storage/schedule/ScheduleJpaRepository.java @@ -1,6 +1,6 @@ package com.mangoboss.storage.schedule; -import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection; +import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -26,11 +26,18 @@ public interface ScheduleJpaRepository extends JpaRepository findAllByStaffIdAndWorkDate(Long staffId, LocalDate date); - @Query("SELECT s FROM ScheduleEntity s " + - "LEFT JOIN s.attendance a " + - "WHERE s.endTime <= :standardTime " + - "AND (a.clockOutStatus is NULL OR a is NULL) ") - List findAllSchedulesWithoutClockOut(LocalDateTime standardTime); + @Query( + """ + SELECT s AS schedule, f.name AS staffName, st.boss.id AS bossId, st.id AS storeId + FROM ScheduleEntity s + JOIN StaffEntity f ON s.staff.id = f.id + JOIN StoreEntity st ON f.store.id = st.id + LEFT JOIN s.attendance a + WHERE s.endTime <= :standardTime + AND (a.clockOutStatus is NULL OR a is NULL) + """ + ) + List findAllSchedulesWithoutClockOut(LocalDateTime standardTime); Boolean existsByRegularGroupId(Long regularGroupId); @@ -58,5 +65,5 @@ AND NOT EXISTS ( AND n.metaId = s.id ) """) - List findLateSchedulesWithoutAlarm(LocalDateTime standardTime); + List findLateSchedulesWithoutAlarm(LocalDateTime standardTime); } diff --git a/storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForLateClockInProjection.java b/storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForNotificationProjection.java similarity index 79% rename from storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForLateClockInProjection.java rename to storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForNotificationProjection.java index 8e5a1610..b0caae2e 100644 --- a/storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForLateClockInProjection.java +++ b/storage/src/main/java/com/mangoboss/storage/schedule/projection/ScheduleForNotificationProjection.java @@ -2,9 +2,12 @@ import com.mangoboss.storage.schedule.ScheduleEntity; -public interface ScheduleForLateClockInProjection { +public interface ScheduleForNotificationProjection { ScheduleEntity getSchedule(); + String getStaffName(); + Long getBossId(); + Long getStoreId(); } From 63a2518a484ada65adca3c48996733170e368d4a Mon Sep 17 00:00:00 2001 From: hyunzzii Date: Mon, 9 Jun 2025 15:36:27 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20store=20=EC=88=98=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9A=94=EA=B8=88=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/facade/store/BossStoreFacade.java | 1 - .../subscription/SubscriptionFacade.java | 6 +- .../app/common/exception/CustomErrorInfo.java | 1 + .../repository/SubscriptionRepository.java | 2 +- .../domain/service/store/StoreService.java | 8 + .../subscription/SubscriptionService.java | 8 +- .../SubscriptionRepositoryImpl.java | 4 +- .../mangoboss/storage/user/UserEntity.java | 138 +++++++++--------- 8 files changed, 94 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/com/mangoboss/app/api/facade/store/BossStoreFacade.java b/app/src/main/java/com/mangoboss/app/api/facade/store/BossStoreFacade.java index 949d06dc..809bffde 100644 --- a/app/src/main/java/com/mangoboss/app/api/facade/store/BossStoreFacade.java +++ b/app/src/main/java/com/mangoboss/app/api/facade/store/BossStoreFacade.java @@ -48,7 +48,6 @@ public class BossStoreFacade { @Transactional public StoreCreateResponse createStore(final Long userId, final StoreCreateRequest request) { final UserEntity boss = userService.getUserById(userId); - storeService.validateBusinessNumber(request.businessNumber()); final StoreEntity saved = storeService.createStore(request, boss); payrollSettingService.initPayrollSettingForStore(saved); requiredDocumentService.initRequiredDocuments(saved); diff --git a/app/src/main/java/com/mangoboss/app/api/facade/subscription/SubscriptionFacade.java b/app/src/main/java/com/mangoboss/app/api/facade/subscription/SubscriptionFacade.java index 1d176f2a..dc551627 100644 --- a/app/src/main/java/com/mangoboss/app/api/facade/subscription/SubscriptionFacade.java +++ b/app/src/main/java/com/mangoboss/app/api/facade/subscription/SubscriptionFacade.java @@ -2,9 +2,11 @@ import com.mangoboss.app.domain.service.billing.BillingService; import com.mangoboss.app.domain.service.subscription.SubscriptionService; +import com.mangoboss.app.domain.service.user.UserService; import com.mangoboss.app.dto.subscription.request.SubscriptionCreateRequest; import com.mangoboss.app.dto.subscription.response.SubscriptionOrderResponse; import com.mangoboss.app.dto.subscription.response.SubscriptionResponse; +import com.mangoboss.storage.user.UserEntity; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -16,10 +18,12 @@ public class SubscriptionFacade { private final SubscriptionService subscriptionService; private final BillingService billingService; + private final UserService userService; public void createOrReplaceSubscription(Long bossId, SubscriptionCreateRequest request) { billingService.validateBillingExists(bossId); - subscriptionService.createOrReplaceSubscription(bossId, request.planType()); + UserEntity boss = userService.getUserById(bossId); + subscriptionService.createOrReplaceSubscription(boss, request.planType()); } public SubscriptionResponse getSubscription(Long bossId) { diff --git a/app/src/main/java/com/mangoboss/app/common/exception/CustomErrorInfo.java b/app/src/main/java/com/mangoboss/app/common/exception/CustomErrorInfo.java index 28245d67..6f2140d9 100644 --- a/app/src/main/java/com/mangoboss/app/common/exception/CustomErrorInfo.java +++ b/app/src/main/java/com/mangoboss/app/common/exception/CustomErrorInfo.java @@ -68,6 +68,7 @@ public enum CustomErrorInfo { DOCUMENT_NOT_BELONG_TO_STAFF(403, "이 서류는 해당 알바생의 서류가 아닙니다.", 403006), FORBIDDEN_TASK_LOG_DELETE(403, "해당 업무 완료 기록을 삭제할 권한이 없습니다.", 403007), WORK_REPORT_ACCESS_DENIED(403, "보고사항 접근 권한이 없습니다.", 403008), + PLAN_LIMIT_EXCEEDED(403, "무료 요금제는 매장 2개, 알바생 5명 까지만 가능합니다.",403009), // 422 Unprocessable Entity CONTRACT_PDF_TAMPERED(422, "계약서 PDF의 무결성이 손상되었습니다.", 422001), diff --git a/app/src/main/java/com/mangoboss/app/domain/repository/SubscriptionRepository.java b/app/src/main/java/com/mangoboss/app/domain/repository/SubscriptionRepository.java index e826abf1..12e0a364 100644 --- a/app/src/main/java/com/mangoboss/app/domain/repository/SubscriptionRepository.java +++ b/app/src/main/java/com/mangoboss/app/domain/repository/SubscriptionRepository.java @@ -5,7 +5,7 @@ import java.util.Optional; public interface SubscriptionRepository { - void save(SubscriptionEntity subscriptionEntity); + SubscriptionEntity save(SubscriptionEntity subscriptionEntity); Optional findByBossId(Long bossId); void delete(SubscriptionEntity subscriptionEntity); boolean existsByBossId(Long bossId); diff --git a/app/src/main/java/com/mangoboss/app/domain/service/store/StoreService.java b/app/src/main/java/com/mangoboss/app/domain/service/store/StoreService.java index cb7bdfdc..e0bc97e8 100644 --- a/app/src/main/java/com/mangoboss/app/domain/service/store/StoreService.java +++ b/app/src/main/java/com/mangoboss/app/domain/service/store/StoreService.java @@ -23,6 +23,7 @@ @Transactional public class StoreService { private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int PLAN_LIMIT_STORE_NUM = 2; private static final int INVITE_CODE_LENGTH = 6; private static final int QR_CODE_LENGTH = 12; @@ -58,6 +59,9 @@ public StoreEntity getStoreById(final Long storeId) { @Transactional public StoreEntity createStore(final StoreCreateRequest request, final UserEntity boss) { + if (boss.getSubscription() == null && getStoresNum(boss.getId()) >= PLAN_LIMIT_STORE_NUM){ + throw new CustomException(CustomErrorInfo.PLAN_LIMIT_EXCEEDED); + } validateBusinessNumber(request.businessNumber()); final String inviteCode = generateInviteCode(); final String attendanceQrCode = generateQrCode(); @@ -65,6 +69,10 @@ public StoreEntity createStore(final StoreCreateRequest request, final UserEntit return storeRepository.save(store); } + private Integer getStoresNum(final Long bossId) { + return storeRepository.findAllByBossId(bossId).size(); + } + private String generateInviteCode() { String code; do { diff --git a/app/src/main/java/com/mangoboss/app/domain/service/subscription/SubscriptionService.java b/app/src/main/java/com/mangoboss/app/domain/service/subscription/SubscriptionService.java index eb75c94a..bcc888b1 100644 --- a/app/src/main/java/com/mangoboss/app/domain/service/subscription/SubscriptionService.java +++ b/app/src/main/java/com/mangoboss/app/domain/service/subscription/SubscriptionService.java @@ -8,6 +8,7 @@ import com.mangoboss.storage.subscription.SubscriptionEntity; import com.mangoboss.app.domain.repository.SubscriptionRepository; import com.mangoboss.storage.subscription.SubscriptionOrderEntity; +import com.mangoboss.storage.user.UserEntity; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,11 +24,12 @@ public class SubscriptionService { private final SubscriptionOrderRepository subscriptionOrderRepository; @Transactional - public void createOrReplaceSubscription(Long bossId, PlanType planType) { - subscriptionRepository.findByBossId(bossId).ifPresent(subscriptionRepository::delete); + public void createOrReplaceSubscription(UserEntity boss, PlanType planType) { + subscriptionRepository.findByBossId(boss.getId()).ifPresent(subscriptionRepository::delete); - SubscriptionEntity subscription = SubscriptionEntity.create(bossId, planType); + SubscriptionEntity subscription = SubscriptionEntity.create(boss.getId(), planType); subscriptionRepository.save(subscription); + boss.addSubscription(subscription); } public SubscriptionEntity getSubscription(Long bossId) { diff --git a/app/src/main/java/com/mangoboss/app/infra/persistence/SubscriptionRepositoryImpl.java b/app/src/main/java/com/mangoboss/app/infra/persistence/SubscriptionRepositoryImpl.java index c81f3b88..4242e0bc 100644 --- a/app/src/main/java/com/mangoboss/app/infra/persistence/SubscriptionRepositoryImpl.java +++ b/app/src/main/java/com/mangoboss/app/infra/persistence/SubscriptionRepositoryImpl.java @@ -14,8 +14,8 @@ public class SubscriptionRepositoryImpl implements SubscriptionRepository { private final SubscriptionJpaRepository subscriptionJpaRepository; @Override - public void save(SubscriptionEntity subscriptionEntity) { - subscriptionJpaRepository.save(subscriptionEntity); + public SubscriptionEntity save(SubscriptionEntity subscriptionEntity) { + return subscriptionJpaRepository.save(subscriptionEntity); } @Override diff --git a/storage/src/main/java/com/mangoboss/storage/user/UserEntity.java b/storage/src/main/java/com/mangoboss/storage/user/UserEntity.java index 9c6aa15f..1f0ff325 100644 --- a/storage/src/main/java/com/mangoboss/storage/user/UserEntity.java +++ b/storage/src/main/java/com/mangoboss/storage/user/UserEntity.java @@ -1,17 +1,13 @@ package com.mangoboss.storage.user; import com.mangoboss.storage.BaseTimeEntity; + import java.time.LocalDate; import java.time.LocalDateTime; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import com.mangoboss.storage.schedule.ScheduleEntity; +import com.mangoboss.storage.subscription.SubscriptionEntity; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -22,62 +18,72 @@ @Entity @Table(name = "\"user\"") public class UserEntity extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") - private Long id; - - @Column(unique = true, nullable = false) - private String email; - - @Column(nullable = false) - private String name; - - private String password; - - @Column(nullable = false) - private String phone; - - @Column(unique = true, nullable = false) - private Long kakaoId; - - @Column(nullable = false) - private LocalDate birth; - - @Column(nullable = false) - private String profileImageUrl; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Role role; - - @Builder - private UserEntity(final Long kakaoId, final String name, final String email, final String phone, - final LocalDate birth, final String profileImageUrl, final Role role, final LocalDateTime createdAt - ) { - this.kakaoId = kakaoId; - this.name = name; - this.email = email; - this.phone = phone; - this.birth = birth; - this.profileImageUrl = profileImageUrl; - this.role = role; - } - - public static UserEntity create(final Long kakaoId, final String name, final String email, final String phone, - final LocalDate birth, final String profileImageUrl, final Role role) { - return UserEntity.builder() - .kakaoId(kakaoId) - .name(name) - .email(email) - .phone(phone) - .birth(birth) - .profileImageUrl(profileImageUrl) - .role(role) - .build(); - } - - public void assignRole(final Role newRole) { - this.role = newRole; - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String name; + + private String password; + + @Column(nullable = false) + private String phone; + + @Column(unique = true, nullable = false) + private Long kakaoId; + + @Column(nullable = false) + private LocalDate birth; + + @Column(nullable = false) + private String profileImageUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "subscription_id", nullable = true) + private SubscriptionEntity subscription; + + @Builder + private UserEntity(final Long kakaoId, final String name, final String email, final String phone, + final LocalDate birth, final String profileImageUrl, final Role role, final SubscriptionEntity subscription + ) { + this.kakaoId = kakaoId; + this.name = name; + this.email = email; + this.phone = phone; + this.birth = birth; + this.profileImageUrl = profileImageUrl; + this.role = role; + this.subscription = subscription; + } + + public static UserEntity create(final Long kakaoId, final String name, final String email, final String phone, + final LocalDate birth, final String profileImageUrl, final Role role) { + return UserEntity.builder() + .kakaoId(kakaoId) + .name(name) + .email(email) + .phone(phone) + .birth(birth) + .profileImageUrl(profileImageUrl) + .role(role) + .subscription(null) + .build(); + } + + public void assignRole(final Role newRole) { + this.role = newRole; + } + + public void addSubscription(final SubscriptionEntity subscription) { + this.subscription = subscription; + } } \ No newline at end of file From 42db495c9648e4a19b0a76cf29b7b749c9747f18 Mon Sep 17 00:00:00 2001 From: hyunzzii Date: Mon, 9 Jun 2025 15:58:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20staff=20=EC=88=98=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9A=94=EA=B8=88=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/facade/store/StaffStoreFacade.java | 2 +- .../app/domain/service/staff/StaffService.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/mangoboss/app/api/facade/store/StaffStoreFacade.java b/app/src/main/java/com/mangoboss/app/api/facade/store/StaffStoreFacade.java index 23310511..c8de2532 100644 --- a/app/src/main/java/com/mangoboss/app/api/facade/store/StaffStoreFacade.java +++ b/app/src/main/java/com/mangoboss/app/api/facade/store/StaffStoreFacade.java @@ -26,7 +26,7 @@ public class StaffStoreFacade { public StaffJoinResponse joinStaff(final Long userId, final StaffJoinRequest request) { final UserEntity user = userService.getUserById(userId); final StoreEntity store = storeService.getStoreByInviteCode(request.inviteCode()); - staffService.createStaff(user, store); + staffService.createStaff(user, store, store.getBoss()); return StaffJoinResponse.fromEntity(store); } diff --git a/app/src/main/java/com/mangoboss/app/domain/service/staff/StaffService.java b/app/src/main/java/com/mangoboss/app/domain/service/staff/StaffService.java index aed24385..bc647027 100644 --- a/app/src/main/java/com/mangoboss/app/domain/service/staff/StaffService.java +++ b/app/src/main/java/com/mangoboss/app/domain/service/staff/StaffService.java @@ -19,15 +19,23 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class StaffService { + private static final int PLAN_LIMIT_STAFF_NUM = 5; private final StaffRepository staffRepository; @Transactional - public StaffEntity createStaff(final UserEntity user, final StoreEntity store) { + public StaffEntity createStaff(final UserEntity user, final StoreEntity store, final UserEntity boss) { isAlreadyJoin(user, store); + if (boss.getSubscription() == null && getStaffsNum(store.getId()) >= PLAN_LIMIT_STAFF_NUM) { + throw new CustomException(CustomErrorInfo.PLAN_LIMIT_EXCEEDED); + } final StaffEntity staff = StaffEntity.create(user, store); return staffRepository.save(staff); } + private Integer getStaffsNum(final Long storeId) { + return getStaffsForStore(storeId).size(); + } + public StaffEntity validateStaffBelongsToStore(final Long storeId, final Long staffId) { return staffRepository.getByIdAndStoreId(staffId, storeId); } From 489db0d4aed73f10da27746c5201cb18ab5bdb60 Mon Sep 17 00:00:00 2001 From: hyunzzii Date: Mon, 9 Jun 2025 16:06:05 +0900 Subject: [PATCH 5/5] =?UTF-8?q?hotfix:=20app=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 1 + .../app/domain/service/staff/StaffServiceTest.java | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 513479ff..ea7882e4 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -143,6 +143,7 @@ jobs: echo "TOSS_PAYMENT_SECRET_KEY=${{ secrets.TOSS_PAYMENT_SECRET_KEY }}" >> .env echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env + echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin diff --git a/app/src/test/java/com/mangoboss/app/domain/service/staff/StaffServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/staff/StaffServiceTest.java index 67516806..6fcf39cb 100644 --- a/app/src/test/java/com/mangoboss/app/domain/service/staff/StaffServiceTest.java +++ b/app/src/test/java/com/mangoboss/app/domain/service/staff/StaffServiceTest.java @@ -33,10 +33,11 @@ class StaffServiceTest { UserEntity user = mock(UserEntity.class); StoreEntity store = mock(StoreEntity.class); StaffEntity staff = mock(StaffEntity.class); + UserEntity boss = mock(UserEntity.class); when(staffRepository.save(any(StaffEntity.class))).thenReturn(staff); //when - StaffEntity result = staffService.createStaff(user,store); + StaffEntity result = staffService.createStaff(user,store,boss); //then assertThat(result).isEqualTo(staff); @@ -47,11 +48,12 @@ class StaffServiceTest { //given UserEntity user = mock(UserEntity.class); StoreEntity store = mock(StoreEntity.class); + UserEntity boss = mock(UserEntity.class); when(staffRepository.existsByUserIdAndStoreId(any(Long.class), any(Long.class))).thenReturn(true); //when //then - Assertions.assertThatThrownBy(()-> staffService.createStaff(user,store)) + Assertions.assertThatThrownBy(()-> staffService.createStaff(user,store,boss)) .isInstanceOf(CustomException.class) .hasMessage(CustomErrorInfo.ALREADY_JOIN_STAFF.getMessage()); }