diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java index c5b5c6a3..80d977a2 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java @@ -1,9 +1,25 @@ package kr.ac.kookmin.stream.member.domain.member.domain; +import java.util.Arrays; +import kr.ac.kookmin.stream.common.BusinessException; +import lombok.AllArgsConstructor; + /** * 학부. 학생회 부서(CouncilDepartment)와는 다른 개념이다. */ +@AllArgsConstructor public enum Department { - AI, - SW + AI("인공지능전공"), + SW("소프트웨어전공"); + + // 로그인 provider가 주는 소속 문자열에서 찾는 전공명 + private final String majorName; + + /** 소속 문자열을 학부로 바꾼다. 소프트웨어융합대학 전공이 아니면 가입할 수 없다. */ + public static Department fromMajor(String major) { + return Arrays.stream(values()) + .filter(department -> major != null && major.contains(department.majorName)) + .findFirst() + .orElseThrow(() -> new BusinessException(MemberErrorCode.DEPARTMENT_NOT_ALLOWED)); + } } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java index ef5deb80..75f43d22 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java @@ -16,21 +16,36 @@ public class Member { private String studentId; private String name; private Department department; + // 로그인 provider가 주는 학적 상태 원문(재학·휴학·졸업 등). 로그인 전 이관 회원은 null이다 + private String academicStatus; private String email; private String fcmToken; private Role role; private CouncilDepartment councilDepartment; + // 로그인으로 처음 가입하는 회원. 운영진 권한·학생회 부서는 가입 뒤 따로 부여한다 + public static Member create(String studentId, String name, Department department, String academicStatus) { + return new Member(null, studentId, name, department, academicStatus, null, null, Role.STUDENT, null); + } + public static Member of( Long id, String studentId, String name, Department department, + String academicStatus, String email, String fcmToken, Role role, CouncilDepartment councilDepartment ) { - return new Member(id, studentId, name, department, email, fcmToken, role, councilDepartment); + return new Member(id, studentId, name, department, academicStatus, email, fcmToken, role, councilDepartment); + } + + // 로그인할 때마다 provider의 최신 이름·학부·학적 상태로 갱신한다 + public void updateProfile(String name, Department department, String academicStatus) { + this.name = name; + this.department = department; + this.academicStatus = academicStatus; } } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java index 5df8fbb3..1bfc9911 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java @@ -11,7 +11,8 @@ @AllArgsConstructor public enum MemberErrorCode implements ErrorCode { - MEMBER_NOT_FOUND(ErrorStatus.NOT_FOUND, "회원을 찾을 수 없습니다."); + MEMBER_NOT_FOUND(ErrorStatus.NOT_FOUND, "회원을 찾을 수 없습니다."), + DEPARTMENT_NOT_ALLOWED(ErrorStatus.FORBIDDEN, "소프트웨어융합대학 학생만 이용할 수 있습니다."); private final int status; private final String message; diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberProfileCommand.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberProfileCommand.java new file mode 100644 index 00000000..a1552128 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberProfileCommand.java @@ -0,0 +1,11 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +/** + * 로그인 provider가 준 회원 프로필. major·academicStatus는 원문이며, 학부 변환은 {@link Department#fromMajor}가 한다. + */ +public record MemberProfileCommand( + String studentId, + String name, + String major, + String academicStatus +) {} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java index 49c027fc..c7e98d8e 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java @@ -6,6 +6,8 @@ public interface MemberRepository { Optional findById(Long id); + Optional findByStudentId(String studentId); List findAllByIds(List ids); List searchIdsByKeyword(String keyword); + Member save(Member member); } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberTermAgreementRepository.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberTermAgreementRepository.java new file mode 100644 index 00000000..2ad7a2ec --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberTermAgreementRepository.java @@ -0,0 +1,8 @@ +package kr.ac.kookmin.stream.member.domain.member.repository; + +import java.util.List; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberTermAgreement; + +public interface MemberTermAgreementRepository { + List findAllByMemberId(Long memberId); +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java index 46ef4bc6..7cd62f93 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java @@ -3,10 +3,13 @@ import java.util.List; import java.util.Map; import kr.ac.kookmin.stream.member.domain.member.domain.Member; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberProfileCommand; public interface MemberService { Member getById(Long id); List findAllByIds(List ids); Map getMapByIds(List ids); List searchIdsByKeyword(String keyword); + Member updateProfile(Long id, MemberProfileCommand command); + Member registerOrUpdateByStudentId(MemberProfileCommand command); } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberTermService.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberTermService.java new file mode 100644 index 00000000..0abd77db --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberTermService.java @@ -0,0 +1,5 @@ +package kr.ac.kookmin.stream.member.domain.member.service; + +public interface MemberTermService { + boolean hasAgreedRequiredTerms(Long memberId); +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java index b41d13e9..19d90ac7 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java @@ -5,12 +5,15 @@ import java.util.function.Function; import java.util.stream.Collectors; import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.member.domain.member.domain.Department; import kr.ac.kookmin.stream.member.domain.member.domain.Member; import kr.ac.kookmin.stream.member.domain.member.domain.MemberErrorCode; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberProfileCommand; import kr.ac.kookmin.stream.member.domain.member.repository.MemberRepository; import kr.ac.kookmin.stream.member.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -40,4 +43,25 @@ public Map getMapByIds(List ids) { public List searchIdsByKeyword(String keyword) { return memberRepository.searchIdsByKeyword(keyword); } + + @Override + @Transactional + public Member updateProfile(Long id, MemberProfileCommand command) { + Member member = getById(id); + member.updateProfile(command.name(), Department.fromMajor(command.major()), command.academicStatus()); + return memberRepository.save(member); + } + + @Override + @Transactional + public Member registerOrUpdateByStudentId(MemberProfileCommand command) { + Department department = Department.fromMajor(command.major()); + Member member = memberRepository.findByStudentId(command.studentId()) + .map(existing -> { + existing.updateProfile(command.name(), department, command.academicStatus()); + return existing; + }) + .orElseGet(() -> Member.create(command.studentId(), command.name(), department, command.academicStatus())); + return memberRepository.save(member); + } } diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberTermServiceImpl.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberTermServiceImpl.java new file mode 100644 index 00000000..2fdff165 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberTermServiceImpl.java @@ -0,0 +1,30 @@ +package kr.ac.kookmin.stream.member.domain.member.service.impl; + +import java.util.EnumSet; +import java.util.Set; +import java.util.stream.Collectors; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberTermAgreement; +import kr.ac.kookmin.stream.member.domain.member.domain.TermType; +import kr.ac.kookmin.stream.member.domain.member.repository.MemberTermAgreementRepository; +import kr.ac.kookmin.stream.member.domain.member.service.MemberTermService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class MemberTermServiceImpl implements MemberTermService { + + private static final Set REQUIRED_TERM_TYPES = + EnumSet.of(TermType.PRIVACY_POLICY, TermType.TERMS_OF_SERVICE); + + private final MemberTermAgreementRepository memberTermAgreementRepository; + + @Override + public boolean hasAgreedRequiredTerms(Long memberId) { + Set agreedTermTypes = memberTermAgreementRepository.findAllByMemberId(memberId).stream() + .filter(MemberTermAgreement::isAgreed) + .map(MemberTermAgreement::getTermType) + .collect(Collectors.toSet()); + return agreedTermTypes.containsAll(REQUIRED_TERM_TYPES); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java index 0f68a633..a3069ec3 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java @@ -38,6 +38,9 @@ public class MemberJpaEntity extends BaseSoftDeleteEntity { @Column(nullable = false, length = 30) private Department department; + @Column(name = "academic_status", length = 50) + private String academicStatus; + private String email; @Column(name = "fcm_token") @@ -56,6 +59,7 @@ private MemberJpaEntity(Member member) { this.studentId = member.getStudentId(); this.name = member.getName(); this.department = member.getDepartment(); + this.academicStatus = member.getAcademicStatus(); this.email = member.getEmail(); this.fcmToken = member.getFcmToken(); this.role = member.getRole(); @@ -67,6 +71,6 @@ public static MemberJpaEntity from(Member member) { } public Member toDomain() { - return Member.of(id, studentId, name, department, email, fcmToken, role, councilDepartment); + return Member.of(id, studentId, name, department, academicStatus, email, fcmToken, role, councilDepartment); } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java index 24fbf7ab..2877d272 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java @@ -10,6 +10,8 @@ public interface MemberJpaRepository extends JpaRepository findByIdAndIsDeletedFalse(Long id); + Optional findByStudentIdAndIsDeletedFalse(String studentId); + List findAllByIdInAndIsDeletedFalse(List ids); @Query(""" diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java index 88e7e1a2..89998df2 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java @@ -18,6 +18,11 @@ public Optional findById(Long id) { return memberJpaRepository.findByIdAndIsDeletedFalse(id).map(MemberJpaEntity::toDomain); } + @Override + public Optional findByStudentId(String studentId) { + return memberJpaRepository.findByStudentIdAndIsDeletedFalse(studentId).map(MemberJpaEntity::toDomain); + } + @Override public List findAllByIds(List ids) { if (ids.isEmpty()) { @@ -32,4 +37,9 @@ public List findAllByIds(List ids) { public List searchIdsByKeyword(String keyword) { return memberJpaRepository.searchIdsByKeyword(keyword); } + + @Override + public Member save(Member member) { + return memberJpaRepository.save(MemberJpaEntity.from(member)).toDomain(); + } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaRepository.java new file mode 100644 index 00000000..35637f1f --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaRepository.java @@ -0,0 +1,8 @@ +package kr.ac.kookmin.stream.db.member; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberTermAgreementJpaRepository extends JpaRepository { + List findAllByMemberId(Long memberId); +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementRepositoryImpl.java new file mode 100644 index 00000000..42d8f3b0 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementRepositoryImpl.java @@ -0,0 +1,21 @@ +package kr.ac.kookmin.stream.db.member; + +import java.util.List; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberTermAgreement; +import kr.ac.kookmin.stream.member.domain.member.repository.MemberTermAgreementRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MemberTermAgreementRepositoryImpl implements MemberTermAgreementRepository { + + private final MemberTermAgreementJpaRepository memberTermAgreementJpaRepository; + + @Override + public List findAllByMemberId(Long memberId) { + return memberTermAgreementJpaRepository.findAllByMemberId(memberId).stream() + .map(MemberTermAgreementJpaEntity::toDomain) + .toList(); + } +} diff --git a/infrastructure/db/src/main/resources/db/migration/V14__add_academic_status_to_members.sql b/infrastructure/db/src/main/resources/db/migration/V14__add_academic_status_to_members.sql new file mode 100644 index 00000000..151c6acc --- /dev/null +++ b/infrastructure/db/src/main/resources/db/migration/V14__add_academic_status_to_members.sql @@ -0,0 +1,4 @@ +-- 로그인 provider가 주는 학적 상태 원문(재학·휴학·졸업 등). 로그인할 때마다 갱신한다. +-- 로그인 전인 기존(이관) 회원은 값이 없으므로 NULL을 허용한다. +ALTER TABLE members + ADD COLUMN academic_status VARCHAR(50) NULL AFTER department;