-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#84] 로그인용 회원 가입·갱신·약관 동의 조회·학적 상태 추가 #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/#83-oauth-auth-domain
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift 프로필 변경을 불변 도메인 객체 반환으로 구현해 주세요. 추가한 메서드는 경로 지침에 포함된 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MemberTermAgreement> findAllByMemberId(Long memberId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package kr.ac.kookmin.stream.member.domain.member.service; | ||
|
|
||
| public interface MemberTermService { | ||
| boolean hasAgreedRequiredTerms(Long memberId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Long, Member> getMapByIds(List<Long> ids) { | |
| public List<Long> 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updateProfile이 로그인할 때마다 이루어진다면, 매번 DB에 update 쿼리가 나가 쓰기 연산을 하게 될 것 같습니다. 이전 값과 비교하는 로직을 추가해서 변경된 값이 있을 때만 save()를 실행하는 방식은 어떨까요? |
||
| } | ||
|
|
||
| @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())); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- MemberServiceImpl ---'
cat -n core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java
printf '%s\n' '--- TransactionTemplate and transaction usage ---'
rg -n --glob '*.java' --glob '*.kt' 'TransactionTemplate|PlatformTransactionManager|@Transactional|Propagation\.' .
printf '%s\n' '--- transaction dependencies and Spring versions ---'
rg -n --glob 'build.gradle*' --glob 'gradle.properties' --glob 'libs.versions.toml' 'spring-tx|spring-framework|springBoot|spring\.boot|org\.springframework' .
printf '%s\n' '--- registerOrUpdateByStudentId references ---'
rg -n --glob '*.java' 'registerOrUpdateByStudentId|MemberService' core infrastructure app . 2>/dev/null | head -200
printf '%s\n' '--- transaction-related tests ---'
rg -n --glob '*Test.java' 'TransactionTemplate|transaction|registerOrUpdateByStudentId|active_student_id|Duplicate|DataIntegrityViolation' core infrastructure . 2>/dev/null | head -200Repository: billilge/stream-server Length of output: 18737 🌐 Web query:
💡 Result: 동일 학번의 동시 신규 등록을 제한적으로 재시도하세요. 동일 학번의 두 조회가 모두 빈 결과를 반환하면 두 트랜잭션이 신규 회원을 저장할 수 있습니다. 이 문제는 동시 최초 등록에 한정된 일시적 실패입니다. 호출 경계에서 트랜잭션 전체를 제한된 횟수로 재시도하면 됩니다. 현재 서비스에 재시도 시설은 없으므로, 원자적 upsert 대신 작은 재시도 헬퍼와 동시 최초 등록 통합 테스트를 추가하는 국소 수정으로 해결할 수 있습니다. 🤖 Prompt for AI Agents |
||
| return memberRepository.save(member); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TermType> REQUIRED_TERM_TYPES = | ||
| EnumSet.of(TermType.PRIVACY_POLICY, TermType.TERMS_OF_SERVICE); | ||
|
|
||
| private final MemberTermAgreementRepository memberTermAgreementRepository; | ||
|
|
||
| @Override | ||
| public boolean hasAgreedRequiredTerms(Long memberId) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 조회 메서드에 읽기 전용 트랜잭션을 선언하세요.
경로 지침은 “트랜잭션 경계는 Service 메서드에 둔다. 조회 전용은 수정안 @Override
+ @Transactional(readOnly = true)
public boolean hasAgreedRequiredTerms(Long memberId) {다음 import도 추가하세요. import org.springframework.transaction.annotation.Transactional;🤖 Prompt for AI AgentsSource: Path instructions |
||
| Set<TermType> agreedTermTypes = memberTermAgreementRepository.findAllByMemberId(memberId).stream() | ||
| .filter(MemberTermAgreement::isAgreed) | ||
| .map(MemberTermAgreement::getTermType) | ||
| .collect(Collectors.toSet()); | ||
| return agreedTermTypes.containsAll(REQUIRED_TERM_TYPES); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MemberTermAgreementJpaEntity, Long> { | ||
| List<MemberTermAgreementJpaEntity> findAllByMemberId(Long memberId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MemberTermAgreement> findAllByMemberId(Long memberId) { | ||
| return memberTermAgreementJpaRepository.findAllByMemberId(memberId).stream() | ||
| .map(MemberTermAgreementJpaEntity::toDomain) | ||
| .toList(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| -- 로그인 provider가 주는 학적 상태 원문(재학·휴학·졸업 등). 로그인할 때마다 갱신한다. | ||
| -- 로그인 전인 기존(이관) 회원은 값이 없으므로 NULL을 허용한다. | ||
| ALTER TABLE members | ||
| ADD COLUMN academic_status VARCHAR(50) NULL AFTER department; |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 filter 안에 major != null 기준이 들어있어서, 인공지능 전공인지 확인할 때도 체크하고 소프트웨어전공인지 확인할 때도 체크해서 중복으로 검사하게 됩니다.
major != null은 department별로 달라지는 조건이 아니라 입력값 자체에 대한 전제조건이니, 메서드 맨 위에서 한 번만 체크하는 게 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 것 같습니다~ 리뷰 감사드려요!