Skip to content

Commit

Permalink
[TNT-84] feat: 회원가입 기능 구현 (#20)
Browse files Browse the repository at this point in the history
* [TNT-83] refactor: 불필요한 log 삭제, isNull()로 수정

* [TNT-83] refactor: 불필요한 log 삭제, isNull()로 수정

* [TNT-83] refactor: isNull()로 수정

* [TNT-83] refactor: conflict 예외 추가

* [TNT-83] refactor: 소셜 이메일, 소셜 타입 추가

* [TNT-83] refactor: 회원 에러 메시지 추가

* [TNT-83] refactor: 회원 필드 유효성 검사 추가

* [TNT-83] refactor: 스키마 내용 수정

* [TNT-83] refactor: 스키마 내용 수정

* [TNT-83] feat: q 클래스 추가

* [TNT-83] feat: 회원가입 api 컨트롤러 구현

* [TNT-83] feat: 회원가입 요청 dto 구현

* [TNT-83] feat: 회원가입 응답 dto 구현

* [TNT-83] feat: 트레이너 엔티티 구현

* [TNT-83] feat: 회원가입 기능 서비스 구현 중

* [TNT-83] refactor: 폴더명 수정

* [TNT-83] refactor: DeletedAtIsNull로 수정

* Update config

* [TNT-83] feat: pt 목적 엔티티 구현

* [TNT-83] feat: 트레이니 엔티티 구현

* [TNT-83] feat: q 클래스 생성

* [TNT-83] feat: q 클래스 생성

* [TNT-83] feat: 트레이니, pt 목적 레포지토리 생성

* [TNT-83] feat: cause 추가

* [TNT-84] feat: AWS S3, 이미지 처리 세팅 및 구현

* [TNT-84] feat: AWS S3, 이미지 처리 세팅 및 구현

* [TNT-84] feat: AWS S3, 이미지 처리 세팅 및 구현

* [TNT-84] refactor: pt 목적, 주의사항 필드 순서 수정

* [TNT-84] feat: 세션 ID 추가

* [TNT-84] feat: @repository 제거

* [TNT-84] refactor: 프로필 url, 생일 필드 위치 수정

* [TNT-84] refactor: 불필요한 로그 제거

* [TNT-84] refactor: 불필요한 로그 제거

* [TNT-84] feat: 회원가입 로직 구현

* [TNT-84] feat: 이미지 에러 메시지 추가

* [TNT-84] feat: 트레이너/트레이니 프로필 이미지 분기 처리

* [TNT-84] feat: 이미지 확장자 예외 추가

* [TNT-84] refactor: 불필요한 트랜잭션 제거

* [TNT-84] refactor: 파라미터 개행 수정

* Update submodules

* [TNT-84] refactor: 디폴트 이미지 url 수정

* Update submodules

* [TNT-84] feat: 회원가입 테스트 구현

* [TNT-84] feat: 테스트 메서드명 수정

* [TNT-84] feat: testcontainer 설정 추가

* [TNT-84] feat: generated 추가

* [TNT-84] refactor: slf4j 제거

* [TNT-84] feat: gitignore에 generated 추가

* [TNT-84] refactor: nullable 추가 / validation 추가

* [TNT-84] refactor: 검증 로직 추가 / static import 추가

* [TNT-84] refactor: static import 추가

* [TNT-84] refactor: static import 추가

* [TNT-84] feat: 회원, 트레이니 에러 메시지 추가

* [TNT-84] refactor: RequestMapping members 로 수정

* [TNT-84] refactor: S3Adapter 추가 / 이미지 업로드 로직 분리

* Delete QPtTrainerTrainee.java

* Delete QPtGoal.java

* Delete QTrainee.java

* Delete QTrainer.java

* Delete QBaseTimeEntity.java

* Delete QMember.java

* [TNT-84] refactor: DB 저장, S3 업로드 트랜잭션 분리

* [TNT-84] refactor: DB 저장, S3 업로드 트랜잭션 분리

* [TNT-84] refactor: S3Adapter 추가 / 이미지 업로드 로직 분리

* [TNT-84] feat: MemberServiceTest 구현

* [TNT-84] feat: 상수 클래스 구현

* [TNT-84] feat: 소셜 로그인 요청 dto 검증 로직 추가

* [TNT-84] refactor: 불필요 임시 페이지 삭제

* [TNT-84] refactor: 불필요 로직 삭제

* [TNT-84] refactor: auto_increment로 수정

* [TNT-84] refactor: 필수 동의 항목 검증 수정

* [TNT-84] refactor: 불필요 validation 어노테이션 제거

* [TNT-84] refactor: 메서드명 및 dto 리팩토링

* [TNT-84] feat: findByIdAndDeletedAtIsNull 생성

* [TNT-84] feat: findByIdAndDeletedAtIsNull 생성

* [TNT-84] feat: redis 설정 제거

* chore: 엘라스틱 캐시 설정 추가
  • Loading branch information
fakerdeft authored Jan 22, 2025
1 parent c0baeab commit 5144b18
Show file tree
Hide file tree
Showing 43 changed files with 1,656 additions and 139 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ application.yml
application-local.yml
application-dev.yml
application-prod.yml
/src/main/generated/

### NetBeans ###
/nbproject/private/
Expand Down
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// Testcontainer
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
testImplementation "org.testcontainers:testcontainers:1.20.4"
testImplementation "org.testcontainers:junit-jupiter:1.20.4"

// MockWebServer
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'

Expand All @@ -187,4 +192,10 @@ dependencies {

// Firebase
implementation 'com.google.firebase:firebase-admin:9.4.3'

// AWS S3 SDK
implementation 'software.amazon.awssdk:s3:2.30.0'

// Thumbnailator - 이미지 리사이징과 포맷 변환
implementation 'net.coobird:thumbnailator:0.4.20'
}
Empty file modified gradlew
100755 → 100644
Empty file.
10 changes: 0 additions & 10 deletions infra/docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,3 @@ services:
- --skip-character-set-client-handshake
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci

redis:
image: redis:alpine
container_name: redis
# platform: linux/arm64
platform: linux/amd64
restart: always
command: redis-server
ports:
- "6379:6379"
16 changes: 6 additions & 10 deletions src/main/java/com/tnt/application/auth/SessionService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.tnt.application.auth;

import static com.tnt.global.error.model.ErrorMessage.*;
import static io.micrometer.common.util.StringUtils.*;
import static com.tnt.global.error.model.ErrorMessage.AUTHORIZATION_HEADER_ERROR;
import static com.tnt.global.error.model.ErrorMessage.NO_EXIST_SESSION_IN_STORAGE;
import static io.micrometer.common.util.StringUtils.isBlank;
import static java.util.Objects.isNull;

import java.util.concurrent.TimeUnit;

Expand All @@ -12,9 +14,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class SessionService {
Expand All @@ -29,17 +29,13 @@ public String authenticate(HttpServletRequest request) {
String authHeader = request.getHeader(AUTHORIZATION_HEADER);

if (isBlank(authHeader) || !authHeader.startsWith(SESSION_ID_PREFIX)) {
log.error(AUTHORIZATION_HEADER_ERROR.getMessage());

throw new UnauthorizedException(AUTHORIZATION_HEADER_ERROR);
}

String sessionId = authHeader.substring(SESSION_ID_PREFIX.length());
String sessionValue = redisTemplate.opsForValue().get(sessionId);

if (sessionValue == null) {
log.error(NO_EXIST_SESSION_IN_STORAGE.getMessage());

if (isNull(sessionValue)) {
throw new UnauthorizedException(NO_EXIST_SESSION_IN_STORAGE);
}

Expand All @@ -55,7 +51,7 @@ public void createOrUpdateSession(String sessionId, String memberId) {
} else { // 로그인 시 기존 로그인 상태 제거하고 새로운 세션 생성
String existingSessionId = redisTemplate.opsForValue().get(memberId);

if (existingSessionId != null) {
if (!isNull(existingSessionId)) {
removeSession(sessionId);
removeSession(memberId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tnt.application.member;

import static com.tnt.global.error.model.ErrorMessage.*;
import static com.tnt.global.error.model.ErrorMessage.FAILED_TO_FETCH_PRIVATE_KEY;

import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
Expand All @@ -14,9 +14,7 @@
import com.tnt.global.error.exception.OAuthException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class AppleEcdsaKeyProvider implements ECDSAKeyProvider {
Expand All @@ -40,10 +38,9 @@ public ECPrivateKey getPrivateKey() {
byte[] pkcs8EncodedBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
KeyFactory kf = KeyFactory.getInstance("EC");

return (ECPrivateKey)kf.generatePrivate(keySpec);
} catch (Exception e) {
log.error(FAILED_TO_FETCH_PRIVATE_KEY.getMessage(), e);

throw new OAuthException(FAILED_TO_FETCH_PRIVATE_KEY);
}
}
Expand Down
132 changes: 132 additions & 0 deletions src/main/java/com/tnt/application/member/MemberService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.tnt.application.member;

import static com.tnt.domain.constant.Constant.TRAINEE;
import static com.tnt.domain.constant.Constant.TRAINEE_DEFAULT_IMAGE;
import static com.tnt.domain.constant.Constant.TRAINER;
import static com.tnt.domain.constant.Constant.TRAINER_DEFAULT_IMAGE;
import static com.tnt.global.error.model.ErrorMessage.MEMBER_CONFLICT;
import static com.tnt.global.error.model.ErrorMessage.MEMBER_NOT_FOUND;
import static com.tnt.global.error.model.ErrorMessage.UNSUPPORTED_MEMBER_TYPE;
import static io.hypersistence.tsid.TSID.Factory.getTsid;
import static io.micrometer.common.util.StringUtils.isNotBlank;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tnt.application.auth.SessionService;
import com.tnt.domain.member.Member;
import com.tnt.domain.member.SocialType;
import com.tnt.domain.trainee.PtGoal;
import com.tnt.domain.trainee.Trainee;
import com.tnt.domain.trainer.Trainer;
import com.tnt.dto.member.request.SignUpRequest;
import com.tnt.dto.member.response.SignUpResponse;
import com.tnt.global.error.exception.ConflictException;
import com.tnt.global.error.exception.NotFoundException;
import com.tnt.infrastructure.mysql.repository.member.MemberRepository;
import com.tnt.infrastructure.mysql.repository.trainee.PtGoalRepository;
import com.tnt.infrastructure.mysql.repository.trainee.TraineeRepository;
import com.tnt.infrastructure.mysql.repository.trainer.TrainerRepository;

import lombok.RequiredArgsConstructor;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final TrainerRepository trainerRepository;
private final TraineeRepository traineeRepository;
private final PtGoalRepository ptGoalRepository;
private final SessionService sessionService;

@Transactional
public Long signUp(SignUpRequest request) {
validateMemberNotExists(request.socialId(), request.socialType());

return switch (request.memberType()) {
case TRAINER -> createTrainer(request);
case TRAINEE -> createTrainee(request);
default -> throw new IllegalArgumentException(UNSUPPORTED_MEMBER_TYPE.getMessage());
};
}

@Transactional
public SignUpResponse finishSignUpWithImage(String profileImageUrl, Long memberId, String memberType) {
Member member = memberRepository.findByIdAndDeletedAtIsNull(memberId)
.orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND));

member.updateProfileImageUrl(profileImageUrl);

String sessionId = String.valueOf(getTsid());

sessionService.createOrUpdateSession(sessionId, String.valueOf(member.getId()));

return new SignUpResponse(memberType, sessionId, member.getName(), member.getProfileImageUrl());
}

private void validateMemberNotExists(String socialId, String socialType) {
memberRepository.findBySocialIdAndSocialTypeAndDeletedAtIsNull(socialId, SocialType.valueOf(socialType))
.ifPresent(member -> {
throw new ConflictException(MEMBER_CONFLICT);
});
}

private Member createMember(SignUpRequest request, String defaultImageUrl) {
Member member = Member.builder()
.socialId(request.socialId())
.fcmToken(request.fcmToken())
.email(request.socialEmail())
.name(request.name())
.profileImageUrl(defaultImageUrl)
.serviceAgreement(request.serviceAgreement())
.collectionAgreement(request.collectionAgreement())
.advertisementAgreement(request.advertisementAgreement())
.pushAgreement(request.pushAgreement())
.socialType(SocialType.valueOf(request.socialType()))
.build();

return memberRepository.save(member);
}

private Long createTrainer(SignUpRequest request) {
Member member = createMember(request, TRAINER_DEFAULT_IMAGE);
Trainer trainer = Trainer.builder()
.memberId(member.getId())
.build();

trainerRepository.save(trainer);

return member.getId();
}

private Long createTrainee(SignUpRequest request) {
Member member = createMember(request, TRAINEE_DEFAULT_IMAGE);
Trainee trainee = Trainee.builder()
.memberId(member.getId())
.height(request.height())
.weight(request.weight())
.cautionNote(isNotBlank(request.cautionNote()) ? request.cautionNote() : "")
.build();

trainee = traineeRepository.save(trainee);

createPtGoals(trainee, request.goalContents());

return member.getId();
}

private void createPtGoals(Trainee trainee, List<String> goalContents) {
List<PtGoal> ptGoals = goalContents.stream()
.map(content -> PtGoal.builder()
.traineeId(trainee.getId())
.content(content)
.build())
.toList();

ptGoalRepository.saveAll(ptGoals);
}
}
Loading

0 comments on commit 5144b18

Please sign in to comment.