Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public interface AppMemberApi {
@Operation(summary = "회원가입",
description = """
로그인한 회원의 전화번호와 약관 동의 여부를 저장한다.
전화번호는 하이픈을 넣어도 되고 빼도 된다. 필수 약관(PRIVACY_POLICY, TERMS_OF_SERVICE)에 모두 동의해야 한다.""")
전화번호는 하이픈을 넣어도 되고 빼도 된다. 필수 약관(PRIVACY_POLICY, TERMS_OF_SERVICE)에 모두 동의해야 한다.
가입하면 알림 설정이 모두 켜진 상태로 만들어진다.""")
@ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"})
@ApiErrorCode(type = MemberErrorCode.class, codes = {
"MEMBER_NOT_FOUND",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import kr.ac.kookmin.stream.api.app.AppApiUser;
import kr.ac.kookmin.stream.api.app.core.member.request.FcmTokenUpdateRequest;
import kr.ac.kookmin.stream.api.app.core.member.request.MemberSignUpRequest;
import kr.ac.kookmin.stream.api.app.core.member.usecase.MemberSignUpUseCase;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.member.domain.member.service.MemberService;
import lombok.RequiredArgsConstructor;
Expand All @@ -19,14 +20,15 @@
public class AppMemberController implements AppMemberApi {

private final MemberService memberService;
private final MemberSignUpUseCase memberSignUpUseCase;

@Override
@PostMapping("/sign-up")
public ApiResponse<Void> signUp(
AppApiUser apiUser,
@Valid @RequestBody MemberSignUpRequest request
) {
memberService.signUp(apiUser.userId(), request.toCommand());
memberSignUpUseCase.signUp(apiUser.userId(), request.toCommand());
return ApiResponse.success();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.ac.kookmin.stream.api.app.core.member.usecase;

import kr.ac.kookmin.stream.member.domain.member.domain.MemberSignUpCommand;
import kr.ac.kookmin.stream.member.domain.member.service.MemberService;
import kr.ac.kookmin.stream.member.domain.notification.service.MemberNotificationSettingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
* 회원가입. {@code member}(전화번호, 약관 동의)와 {@code notification}(기본 알림 설정) 두 도메인을 조합한다.
*/
@Component
@RequiredArgsConstructor
public class MemberSignUpUseCase {

private final MemberService memberService;
private final MemberNotificationSettingService memberNotificationSettingService;

// 가입을 마친 회원에게는 항상 알림 설정이 있도록 한 트랜잭션으로 묶는다
@Transactional
public void signUp(Long memberId, MemberSignUpCommand command) {
memberService.signUp(memberId, command);
memberNotificationSettingService.createDefaultIfAbsent(memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kr.ac.kookmin.stream.api.app.core.notification;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.ac.kookmin.stream.api.app.AppApiUser;
import kr.ac.kookmin.stream.api.app.core.notification.request.MemberNotificationSettingUpdateRequest;
import kr.ac.kookmin.stream.api.app.core.notification.response.MemberNotificationSettingResponse;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode;
import kr.ac.kookmin.stream.common.CommonErrorCode;
import kr.ac.kookmin.stream.member.domain.notification.domain.NotificationErrorCode;

/**
* 학생 앱 알림 API의 문서 명세. 구현은 {@link AppNotificationController}가 맡는다.
*/
@Tag(name = "알림", description = "학생 앱 알림 설정")
public interface AppNotificationApi {

/** 알림 설정 조회. */
@Operation(summary = "알림 설정 조회",
description = """
로그인한 회원의 카테고리별 푸시 수신 여부를 조회한다.
rentalEnabled는 빌릴게, eventEnabled는 행사, lockerEnabled는 사물함, noticeEnabled는 게시판(새 공지·열린피드백 답변) 알림이다.
전체 알림은 따로 저장하지 않는다. 네 값이 모두 true일 때 켜진 것으로 보여준다.""")
@ApiErrorCode(type = NotificationErrorCode.class, codes = {"NOTIFICATION_SETTING_NOT_FOUND"})
ApiResponse<MemberNotificationSettingResponse> getSetting(AppApiUser apiUser);

/** 알림 설정 수정. */
@Operation(summary = "알림 설정 수정",
description = """
로그인한 회원의 카테고리별 푸시 수신 여부를 바꾼다. 네 값을 모두 보내야 하고, 보낸 값으로 덮어쓴다.
전체 알림을 켜거나 끌 때는 네 값을 모두 true나 false로 보낸다.
꺼진 카테고리의 알림은 푸시로 보내지 않지만 알림함에는 남는다.""")
@ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"})
@ApiErrorCode(type = NotificationErrorCode.class, codes = {"NOTIFICATION_SETTING_NOT_FOUND"})
ApiResponse<MemberNotificationSettingResponse> updateSetting(
AppApiUser apiUser,
MemberNotificationSettingUpdateRequest request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kr.ac.kookmin.stream.api.app.core.notification;

import jakarta.validation.Valid;
import kr.ac.kookmin.stream.api.app.AppApiUser;
import kr.ac.kookmin.stream.api.app.core.notification.request.MemberNotificationSettingUpdateRequest;
import kr.ac.kookmin.stream.api.app.core.notification.response.MemberNotificationSettingResponse;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;
import kr.ac.kookmin.stream.member.domain.notification.service.MemberNotificationSettingService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/app/notifications")
@RequiredArgsConstructor
public class AppNotificationController implements AppNotificationApi {

private final MemberNotificationSettingService memberNotificationSettingService;

@Override
@GetMapping("/settings")
public ApiResponse<MemberNotificationSettingResponse> getSetting(AppApiUser apiUser) {
MemberNotificationSetting setting = memberNotificationSettingService.getByMemberId(apiUser.userId());
return ApiResponse.success(MemberNotificationSettingResponse.from(setting));
}

@Override
@PutMapping("/settings")
public ApiResponse<MemberNotificationSettingResponse> updateSetting(
AppApiUser apiUser,
@Valid @RequestBody MemberNotificationSettingUpdateRequest request
) {
MemberNotificationSetting setting = memberNotificationSettingService.update(apiUser.userId(), request.toCommand());
return ApiResponse.success(MemberNotificationSettingResponse.from(setting));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.ac.kookmin.stream.api.app.core.notification.request;

import jakarta.validation.constraints.NotNull;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSettingUpdateCommand;

public record MemberNotificationSettingUpdateRequest(
@NotNull(message = "빌릴게 알림 수신 여부를 입력해 주세요.")
Boolean rentalEnabled,

@NotNull(message = "행사 알림 수신 여부를 입력해 주세요.")
Boolean eventEnabled,

@NotNull(message = "사물함 알림 수신 여부를 입력해 주세요.")
Boolean lockerEnabled,

@NotNull(message = "게시판 알림 수신 여부를 입력해 주세요.")
Boolean noticeEnabled
) {

public MemberNotificationSettingUpdateCommand toCommand() {
return new MemberNotificationSettingUpdateCommand(rentalEnabled, eventEnabled, lockerEnabled, noticeEnabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.ac.kookmin.stream.api.app.core.notification.response;

import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;

public record MemberNotificationSettingResponse(
boolean rentalEnabled,
boolean eventEnabled,
boolean lockerEnabled,
boolean noticeEnabled
) {
public static MemberNotificationSettingResponse from(MemberNotificationSetting setting) {
return new MemberNotificationSettingResponse(
setting.isRentalEnabled(),
setting.isEventEnabled(),
setting.isLockerEnabled(),
setting.isNoticeEnabled()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ public class MemberNotificationSetting {
private boolean rentalEnabled;
private boolean eventEnabled;
private boolean lockerEnabled;
// 게시판 알림. 새 공지와 열린피드백 답변을 함께 다룬다
private boolean noticeEnabled;

// 회원가입할 때 만든다. 처음에는 모든 알림을 받는다
public static MemberNotificationSetting createDefault(Long memberId) {
return new MemberNotificationSetting(null, memberId, true, true, true, true);
}

public static MemberNotificationSetting of(
Long id,
Long memberId,
Expand All @@ -27,4 +33,19 @@ public static MemberNotificationSetting of(
) {
return new MemberNotificationSetting(id, memberId, rentalEnabled, eventEnabled, lockerEnabled, noticeEnabled);
}

// 바뀐 값이 있으면 true를 돌려준다
public boolean update(MemberNotificationSettingUpdateCommand command) {
if (this.rentalEnabled == command.rentalEnabled()
&& this.eventEnabled == command.eventEnabled()
&& this.lockerEnabled == command.lockerEnabled()
&& this.noticeEnabled == command.noticeEnabled()) {
return false;
}
this.rentalEnabled = command.rentalEnabled();
this.eventEnabled = command.eventEnabled();
this.lockerEnabled = command.lockerEnabled();
this.noticeEnabled = command.noticeEnabled();
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.ac.kookmin.stream.member.domain.notification.domain;

/**
* 알림 설정 수정 요청. 전체 알림은 저장하지 않으므로 네 카테고리 값을 모두 받아 그대로 덮어쓴다.
*/
public record MemberNotificationSettingUpdateCommand(
boolean rentalEnabled,
boolean eventEnabled,
boolean lockerEnabled,
boolean noticeEnabled
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kr.ac.kookmin.stream.member.domain.notification.domain;

import kr.ac.kookmin.stream.common.ErrorCode;
import kr.ac.kookmin.stream.common.ErrorStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Accessors;

@Getter
@Accessors(fluent = true)
@AllArgsConstructor
public enum NotificationErrorCode implements ErrorCode {

NOTIFICATION_SETTING_NOT_FOUND(ErrorStatus.NOT_FOUND, "알림 설정을 찾을 수 없습니다.");

private final int status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.ac.kookmin.stream.member.domain.notification.repository;

import java.util.Optional;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;

public interface MemberNotificationSettingRepository {
Optional<MemberNotificationSetting> findByMemberId(Long memberId);
boolean existsByMemberId(Long memberId);
MemberNotificationSetting save(MemberNotificationSetting setting);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.ac.kookmin.stream.member.domain.notification.service;

import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSettingUpdateCommand;

public interface MemberNotificationSettingService {
MemberNotificationSetting getByMemberId(Long memberId);
void createDefaultIfAbsent(Long memberId);
MemberNotificationSetting update(Long memberId, MemberNotificationSettingUpdateCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package kr.ac.kookmin.stream.member.domain.notification.service.impl;

import kr.ac.kookmin.stream.common.BusinessException;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSettingUpdateCommand;
import kr.ac.kookmin.stream.member.domain.notification.domain.NotificationErrorCode;
import kr.ac.kookmin.stream.member.domain.notification.repository.MemberNotificationSettingRepository;
import kr.ac.kookmin.stream.member.domain.notification.service.MemberNotificationSettingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
class MemberNotificationSettingServiceImpl implements MemberNotificationSettingService {

private final MemberNotificationSettingRepository memberNotificationSettingRepository;

@Override
public MemberNotificationSetting getByMemberId(Long memberId) {
return memberNotificationSettingRepository.findByMemberId(memberId)
.orElseThrow(() -> new BusinessException(NotificationErrorCode.NOTIFICATION_SETTING_NOT_FOUND));
}

// 필수 약관이 늘어 다시 가입하는 회원은 이미 설정이 있으므로 바꾼 값을 그대로 둔다
@Override
@Transactional
public void createDefaultIfAbsent(Long memberId) {
if (memberNotificationSettingRepository.existsByMemberId(memberId)) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재가입 하는 회원의 기존 알림 설정을 유지하기 위한 덮어쓰지 않고 그냥 두는 점 이해했습니다!

memberNotificationSettingRepository.save(MemberNotificationSetting.createDefault(memberId));
}

@Override
@Transactional
public MemberNotificationSetting update(Long memberId, MemberNotificationSettingUpdateCommand command) {
MemberNotificationSetting setting = getByMemberId(memberId);
if (!setting.update(command)) {
return setting;
}
return memberNotificationSettingRepository.save(setting);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.ac.kookmin.stream.db.member;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberNotificationSettingJpaRepository extends JpaRepository<MemberNotificationSettingJpaEntity, Long> {

Optional<MemberNotificationSettingJpaEntity> findByMemberId(Long memberId);

boolean existsByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kr.ac.kookmin.stream.db.member;

import java.util.Optional;
import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting;
import kr.ac.kookmin.stream.member.domain.notification.repository.MemberNotificationSettingRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class MemberNotificationSettingRepositoryImpl implements MemberNotificationSettingRepository {

private final MemberNotificationSettingJpaRepository memberNotificationSettingJpaRepository;

@Override
public Optional<MemberNotificationSetting> findByMemberId(Long memberId) {
return memberNotificationSettingJpaRepository.findByMemberId(memberId)
.map(MemberNotificationSettingJpaEntity::toDomain);
}

@Override
public boolean existsByMemberId(Long memberId) {
return memberNotificationSettingJpaRepository.existsByMemberId(memberId);
}

@Override
public MemberNotificationSetting save(MemberNotificationSetting setting) {
return memberNotificationSettingJpaRepository.save(MemberNotificationSettingJpaEntity.from(setting)).toDomain();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- 알림 설정은 회원가입할 때 만든다. 그 전에 들어온 회원에게도 기본 설정(모두 켜짐)을 만들어 둔다.
-- 아직 가입을 마치지 않은 회원도 포함한다. 나중에 가입하면 이미 있는 설정을 그대로 쓴다.
INSERT INTO member_notification_settings (member_id)
SELECT m.member_id
FROM members m
WHERE m.is_deleted = 0
AND NOT EXISTS (
SELECT 1
FROM member_notification_settings s
WHERE s.member_id = m.member_id
);
Loading