diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0fe8727 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,73 @@ +# 워크플로우의 전체 이름 +name: 픽픽 CI/CD 배포 + +# 워크플로우가 언제 실행될지 정의하는 트리거 +on: + # main 브랜치에 push 이벤트가 발생했을 때 실행 + push: + branches: [ "main" ] + +# 실행될 작업(Job)들을 정의 +jobs: + # '빌드와 배포' 작업 + build-and-deploy: + # 이 작업이 실행될 가상 머신의 종류 (최신 우분투) + runs-on: ubuntu-latest + + # 작업 내에서 순서대로 실행될 단계(Step)들 + steps: + # 1. 소스 코드 체크아웃 + - name: 소스 코드 체크아웃 + uses: actions/checkout@v4 + + # 2. JDK 17 설치 + - name: JDK 17 설치 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # 3. Gradle 캐시 설정 + # 매번 Gradle 의존성 받을 필요 없어 빌드 속도 빨라짐 + - name: Gradle 캐시 설정 + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # 4. gradlew 실행 권한 부여 + - name: gradlew 실행 권한 부여 + run: chmod +x ./gradlew + + # 5. Gradle로 빌드하기 + - name: Gradle로 빌드하기 + run: ./gradlew bootJar + + # 6. EC2에 배포 스크립트와 JAR 파일 업로드 + - name: EC2에 파일 업로드 + uses: appleboy/scp-action@master + with: + host: 3.39.139.208 + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + source: "build/libs/*.jar,deploy.sh" # .jar 파일과 deploy.sh 파일을 함께 업로드 + target: "/home/ubuntu/app" + + # 7. EC2에서 배포 스크립트 실행 + - name: EC2에서 배포 스크립트 실행 + uses: appleboy/ssh-action@master + with: + host: 3.39.139.208 # EC2 퍼블릿 IP + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + envs: | + SPRING_PROFILES_ACTIVE=prod + SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }} + JWT_SECRET=${{ secrets.JWT_SECRET }} + KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} + script: | + cd /home/ubuntu/app + chmod +x deploy.sh # 스크립트 실행 권한 부여 + ./deploy.sh # 스크립트 실행 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0100453 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +.idea +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +src/main/generated/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +.DS_Store \ No newline at end of file diff --git a/.gradle/8.14.3/checksums/checksums.lock b/.gradle/8.14.3/checksums/checksums.lock deleted file mode 100644 index 2333d56..0000000 Binary files a/.gradle/8.14.3/checksums/checksums.lock and /dev/null differ diff --git a/.gradle/8.14.3/checksums/md5-checksums.bin b/.gradle/8.14.3/checksums/md5-checksums.bin deleted file mode 100644 index bc70452..0000000 Binary files a/.gradle/8.14.3/checksums/md5-checksums.bin and /dev/null differ diff --git a/.gradle/8.14.3/checksums/sha1-checksums.bin b/.gradle/8.14.3/checksums/sha1-checksums.bin deleted file mode 100644 index f2d6a0d..0000000 Binary files a/.gradle/8.14.3/checksums/sha1-checksums.bin and /dev/null differ diff --git a/.gradle/8.14.3/executionHistory/executionHistory.bin b/.gradle/8.14.3/executionHistory/executionHistory.bin deleted file mode 100644 index f749ae9..0000000 Binary files a/.gradle/8.14.3/executionHistory/executionHistory.bin and /dev/null differ diff --git a/.gradle/8.14.3/executionHistory/executionHistory.lock b/.gradle/8.14.3/executionHistory/executionHistory.lock deleted file mode 100644 index 633238e..0000000 Binary files a/.gradle/8.14.3/executionHistory/executionHistory.lock and /dev/null differ diff --git a/.gradle/8.14.3/fileChanges/last-build.bin b/.gradle/8.14.3/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/.gradle/8.14.3/fileChanges/last-build.bin and /dev/null differ diff --git a/.gradle/8.14.3/fileHashes/fileHashes.bin b/.gradle/8.14.3/fileHashes/fileHashes.bin deleted file mode 100644 index bdbf38a..0000000 Binary files a/.gradle/8.14.3/fileHashes/fileHashes.bin and /dev/null differ diff --git a/.gradle/8.14.3/fileHashes/fileHashes.lock b/.gradle/8.14.3/fileHashes/fileHashes.lock deleted file mode 100644 index 8d7143b..0000000 Binary files a/.gradle/8.14.3/fileHashes/fileHashes.lock and /dev/null differ diff --git a/.gradle/8.14.3/fileHashes/resourceHashesCache.bin b/.gradle/8.14.3/fileHashes/resourceHashesCache.bin deleted file mode 100644 index f1a2ffe..0000000 Binary files a/.gradle/8.14.3/fileHashes/resourceHashesCache.bin and /dev/null differ diff --git a/.gradle/8.14.3/gc.properties b/.gradle/8.14.3/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 7389f60..0000000 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index 1b57e59..0000000 --- a/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Thu Oct 30 20:59:12 KST 2025 -gradle.version=8.14.3 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin deleted file mode 100644 index bef1e3c..0000000 Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe deleted file mode 100644 index 89e0f3d..0000000 Binary files a/.gradle/file-system.probe and /dev/null differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 104c42f..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 1b387c7..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6ed36dd..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 8405bdb..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1761825546217 - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index f86e8e1..71dfa30 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.7' + id 'org.springframework.boot' version '3.3.13' id 'io.spring.dependency-management' version '1.1.7' } @@ -22,15 +22,46 @@ configurations { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } } dependencies { + // Spring Web MVC implementation 'org.springframework.boot:spring-boot-starter-web' + + // Spring Data JPA + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // Lombok compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // MySQL + runtimeOnly 'com.mysql:mysql-connector-j' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JTokkit + implementation 'com.knuddels:jtokkit:1.1.0' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') // JSON parsing } tasks.named('test') { diff --git a/build/classes/java/main/fitfit/FitfitApplication.class b/build/classes/java/main/fitfit/FitfitApplication.class deleted file mode 100644 index 7dd4383..0000000 Binary files a/build/classes/java/main/fitfit/FitfitApplication.class and /dev/null differ diff --git a/build/resources/main/application.properties b/build/resources/main/application.properties deleted file mode 100644 index b3459a0..0000000 --- a/build/resources/main/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=fitfit diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin deleted file mode 100644 index 7efd6a6..0000000 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and /dev/null differ diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..af7e499 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash + BUILD_JAR=$(ls /home/ubuntu/app/build/libs/*.jar) + JAR_NAME=$(basename $BUILD_JAR) +echo ">>> build file name: $JAR_NAME" >> /home/ubuntu/app/deploy.log + + echo ">>> kill existing process" >> /home/ubuntu/app/deploy.log + PID=$(pgrep -f .jar) + if [ -n "$PID" ]; then + sudo kill -15 $PID + sleep 5 + fi + + echo ">>> execute new jar file" >> /home/ubuntu/app/deploy.log + cd /home/ubuntu/app + nohup sudo java -jar -Dspring.profiles.active=prod $BUILD_JAR > /home/ubuntu/app/application.log 2>&1 & \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..5b6a5d7 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 443 ssl; + server_name api.fitfit.site; + + # SSL 인증서 경로도 api.fitfit.site에 맞게 새로 발급 + # (예: sudo certbot --nginx -d api.fitfit.site) + ssl_certificate /etc/letsencrypt/live/api.fitfit.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.fitfit.site/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + # 요청을 내부 스프링 앱(8080 포트)으로 전달하는 것은 동일합니다. + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name api.fitfit.site; + return 301 https://$host$request_uri; +} diff --git a/src/main/java/fitfit/FitfitApplication.java b/src/main/java/fitfit/FitfitApplication.java index 8830aff..a82b9e9 100644 --- a/src/main/java/fitfit/FitfitApplication.java +++ b/src/main/java/fitfit/FitfitApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class FitfitApplication { public static void main(String[] args) { diff --git a/src/main/java/fitfit/HelloController.java b/src/main/java/fitfit/HelloController.java new file mode 100644 index 0000000..6eba0e7 --- /dev/null +++ b/src/main/java/fitfit/HelloController.java @@ -0,0 +1,16 @@ +package fitfit; + +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@CrossOrigin(origins = "https://fitfit.site") +public class HelloController { + + @GetMapping("/hello") // 1. HTTP GET 요청을 '/hello' 경로와 매핑 + public String getHelloMessage() { + // 2. "환영합니다!" 라는 문자열을 반환 + return "환영합니다!"; + } +} diff --git a/src/main/java/fitfit/domain/kakao/converter/KakaoConverter.java b/src/main/java/fitfit/domain/kakao/converter/KakaoConverter.java new file mode 100644 index 0000000..1941a90 --- /dev/null +++ b/src/main/java/fitfit/domain/kakao/converter/KakaoConverter.java @@ -0,0 +1,27 @@ +package fitfit.domain.kakao.converter; + +import fitfit.domain.member.dto.MemberDataDTO; +import fitfit.domain.member.dto.MemberResponseDTO; +import fitfit.domain.member.entity.Member; +import io.jsonwebtoken.Claims; + +import java.time.LocalDateTime; + +public class KakaoConverter { + + public static MemberDataDTO.MemberData toKakaoMemberData(Claims claims) { + return MemberDataDTO.MemberData.builder() + .sub(claims.getSubject()) + .email(claims.get("email", String.class)) + .build(); + } + + public static MemberResponseDTO.KkoOAuth2LoginResponse toKkoOAuth2LoginResponse(Member member, String accessToken, String refreshToken, LocalDateTime accessTokenExpireAt) { + return MemberResponseDTO.KkoOAuth2LoginResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .accessTokenExpireAt(accessTokenExpireAt) + .memberStatus(member.getStatus()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/fitfit/domain/kakao/service/KakaoOidcService.java b/src/main/java/fitfit/domain/kakao/service/KakaoOidcService.java new file mode 100644 index 0000000..02427f9 --- /dev/null +++ b/src/main/java/fitfit/domain/kakao/service/KakaoOidcService.java @@ -0,0 +1,86 @@ +package fitfit.domain.kakao.service; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import fitfit.domain.kakao.converter.KakaoConverter; +import fitfit.domain.member.dto.MemberDataDTO; +import fitfit.domain.member.dto.MemberRequestDTO; +import fitfit.global.apiPayload.code.status.ErrorStatus; +import fitfit.global.apiPayload.exception.handler.KakaoHandler; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.net.URL; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; + + +@Service +@Slf4j +@RequiredArgsConstructor +public class KakaoOidcService { + // Kakao 공개키 -> Kakao 가 서명한 JWT(id_token) 을 검증하기 위해선 공개키(JWK) 필요 + private static final String JWK_URL = "https://kauth.kakao.com/.well-known/jwks.json"; + // ISSER 상수 -> id_token 안에 들어 있는 iss claim과 비교해 정품 Kakao 토큰인지 검증 + private static final String ISSUER = "https://kauth.kakao.com"; + + /** + * id_token을 검증하고 담겨있는 멤버 데이터 추출하는 메서드 + */ + public MemberDataDTO.MemberData verifyAndParseIdToken(MemberRequestDTO.KkoOAuth2LoginRequest request) { + try { + // JWT 디코드 (헤더 추출) + String[] parts = request.getIdToken().split("\\."); + if (parts.length != 3) throw new KakaoHandler(ErrorStatus.INVALID_JWT_TOKEN); + + // 헤더 에서 kid(키 아이디) 추출 + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); + ObjectMapper mapper = new ObjectMapper(); + JsonNode header = mapper.readTree(headerJson); + String kid = header.get("kid").asText(); + + // JWKS 불러오기 + JsonNode keys = mapper.readTree(new URL(JWK_URL)).get("keys"); + + JsonNode matchedKey = null; + for (JsonNode key : keys) { + if (key.get("kid").asText().equals(kid)) { + matchedKey = key; + break; + } + } + + if (matchedKey == null) throw new KakaoHandler(ErrorStatus.ERROR_ON_VERIFYING); + + // 공개키 추출 (n, e -> RSA public Key) + String n = matchedKey.get("n").asText(); + String e = matchedKey.get("e").asText(); + BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(n)); + BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(e)); + + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec); + + // 검증 및 Claims 추출 + JwtParser parser = Jwts.parserBuilder() + .setSigningKey(publicKey) + .requireIssuer(ISSUER) + .build(); + + Claims claims = parser.parseClaimsJws(request.getIdToken()).getBody(); + + return KakaoConverter.toKakaoMemberData(claims); + } catch (Exception e) { + log.error("KakaoOidcService Error Occurred: {}", e.getMessage()); + throw new KakaoHandler(ErrorStatus.ERROR_ON_VERIFYING); + } + } +} \ No newline at end of file diff --git a/src/main/java/fitfit/domain/member/controller/MemberRestController.java b/src/main/java/fitfit/domain/member/controller/MemberRestController.java new file mode 100644 index 0000000..01fb70e --- /dev/null +++ b/src/main/java/fitfit/domain/member/controller/MemberRestController.java @@ -0,0 +1,59 @@ +package fitfit.domain.member.controller; + +import fitfit.domain.kakao.service.KakaoOidcService; +import fitfit.domain.member.dto.MemberDataDTO; +import fitfit.domain.member.dto.MemberRequestDTO; +import fitfit.domain.member.dto.MemberResponseDTO; +import fitfit.domain.member.entity.Member; +import fitfit.domain.member.service.MemberCommandUseCase; +import fitfit.domain.token.service.MemberTokenCommandUseCase; +import fitfit.global.apiPayload.ApiResponse; +import fitfit.global.enums.Provider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/members") +@RequiredArgsConstructor +@Tag(name = "Member", description = "회원 관련 API") +public class MemberRestController { + + private final MemberCommandUseCase memberCommandUseCase; + private final MemberTokenCommandUseCase memberTokenService; + private final KakaoOidcService kakaoOidcService; + + @PostMapping("/auth/kko") + @Operation(summary = "KAKAO OAuth2 로그인 API", description = "KAKAO OAuth2 로그인 API 입니다.") + public ResponseEntity> kkoOAuth2Login (@Valid @RequestBody MemberRequestDTO.KkoOAuth2LoginRequest request) { + // id_token 검증 후 멤버 데이터 추출 + MemberDataDTO.MemberData kakaoMemberData = kakaoOidcService.verifyAndParseIdToken(request); + + // id_token 에서 추출한 데이터를 통해 멤버 조회 OR 생성 + Member findOrCreateMember = memberCommandUseCase.findOrCreateMember(kakaoMemberData, Provider.KAKAO); + + // 토큰 생성 및 응답 + return ResponseEntity.ok(ApiResponse.onSuccess(memberTokenService.generateKkoLoginToken(findOrCreateMember))); + } + + @PostMapping("/agreements") + @Operation(summary = "약관 동의 API", description = "회원이 약관에 동의하는 API입니다.") + public ResponseEntity> termAgreement( + @RequestHeader(value = "Authorization", required = false) String authorization, + @Valid @RequestBody MemberRequestDTO.TermAgreementRequest request) { + return ResponseEntity.ok(ApiResponse.onSuccess(memberCommandUseCase.termAgreement(authorization, request))); + } + + @PatchMapping("/signup") + @Operation(summary = "회원가입 완료 API", description = "회원의 추가 정보(닉네임, 사용자 커스텀 ID, 성별, 생년월일, 프로필 이미지)를 입력하여 회원가입을 완료하는 API입니다.") + public ResponseEntity> signup( + @RequestHeader(value = "Authorization", required = false) String authorization, + @Valid @RequestBody MemberRequestDTO.MemberSignupRequest request) { + return ResponseEntity.ok(ApiResponse.onSuccess(memberCommandUseCase.memberSignup(authorization, request))); + } +} diff --git a/src/main/java/fitfit/domain/member/converter/MemberConverter.java b/src/main/java/fitfit/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..b67fb60 --- /dev/null +++ b/src/main/java/fitfit/domain/member/converter/MemberConverter.java @@ -0,0 +1,53 @@ +package fitfit.domain.member.converter; + +import fitfit.domain.member.dto.MemberDataDTO; +import fitfit.domain.member.dto.MemberResponseDTO; +import fitfit.domain.member.entity.Member; +import fitfit.domain.member.mapping.MemberTerm; +import fitfit.domain.term.entity.Term; +import fitfit.global.enums.Gender; +import fitfit.global.enums.MemberStatus; +import fitfit.global.enums.Provider; + +public class MemberConverter { + private static final String DEFAULT_NICKNAME = "핏핏이"; + private static final String DEFAULT_PROFILE_IMG_URL = "https://fitfit-profile-img.s3.ap-northeast-2.amazonaws.com/default_img.png"; + private static final String DEFAULT_USER_CUSTOM_ID = "temp_fitfit"; + public static Member toMember (MemberDataDTO.MemberData kakaoMemberData, Provider provider) { + return Member.builder() + .email(kakaoMemberData.getEmail()) + .nickname(DEFAULT_NICKNAME) + .name("가입 중인 사용자") + .phoneNumber("임시 번호") + .userCustomId(DEFAULT_USER_CUSTOM_ID) + .profileImgUrl(DEFAULT_PROFILE_IMG_URL) + .provider(provider) + .providerId(kakaoMemberData.getSub()) + .gender(Gender.NONE) // 다시 추가 + .status(MemberStatus.PENDING) + .build(); + } + + public static MemberResponseDTO.TermAgreementResponse toTermAgreementResponse(Member member) { + return MemberResponseDTO.TermAgreementResponse.builder() + .message("약관 동의가 완료되었습니다.") + .status(MemberStatus.AGREE) + .build(); + } + + public static MemberTerm toMemberTerm(Member member, Term term, Boolean isAgree) { + return MemberTerm.builder() + .member(member) + .term(term) + .isAgree(isAgree) + .build(); + } + + public static MemberResponseDTO.MemberSignupResponse toMemberSignupResponse(Member member) { + return MemberResponseDTO.MemberSignupResponse.builder() + .nickname(member.getNickname()) + .name(member.getName()) + .status(MemberStatus.ACTIVE) + .build(); + } +} diff --git a/src/main/java/fitfit/domain/member/dto/MemberDataDTO.java b/src/main/java/fitfit/domain/member/dto/MemberDataDTO.java new file mode 100644 index 0000000..03c22b5 --- /dev/null +++ b/src/main/java/fitfit/domain/member/dto/MemberDataDTO.java @@ -0,0 +1,17 @@ +package fitfit.domain.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class MemberDataDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MemberData { + private String email; + private String sub; // provider_id + } +} diff --git a/src/main/java/fitfit/domain/member/dto/MemberRequestDTO.java b/src/main/java/fitfit/domain/member/dto/MemberRequestDTO.java new file mode 100644 index 0000000..b5e0541 --- /dev/null +++ b/src/main/java/fitfit/domain/member/dto/MemberRequestDTO.java @@ -0,0 +1,70 @@ +package fitfit.domain.member.dto; + +import fitfit.global.enums.Gender; +import fitfit.global.enums.Style; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +public class MemberRequestDTO { + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class KkoOAuth2LoginRequest { + @NotNull(message = "idToken 은 필수입니다.") + private String idToken; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class RefreshAccessTokenRequest { + @NotNull(message = "refreshToken 은 필수입니다.") + private String refreshToken; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class TermAgreementRequest { + @NotNull(message = "동의 약관 목록은 null일 수 없습니다.") + private List agreeTermIdList; + @NotNull(message = "비동의 약관 목록은 null일 수 없습니다. 빈 배열로 넘겨주세요.") + private List disagreeTermIdList; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class MemberSignupRequest { + @NotNull(message = "닉네임은 필수입니다.") + @Size(max = 25, message = "닉네임은 최대 25자입니다.") + private String nickname; + @NotNull(message = "이름은 필수입니다.") + @Size(max = 25, message = "이름은 최대 25자입니다.") + private String name; // 실제 이름 + @NotNull(message = "키는 필수입니다.") + private String height; + @NotNull(message = "몸무게는 필수입니다.") + private String weight; + @NotNull(message = "휴대폰 번호는 필수입니다.") + private String phoneNumber; + @NotNull(message = "성별은 필수입니다.") + private Gender gender; + @NotNull(message = "생년월일은 필수입니다.") + private LocalDate birth; + // null 가능 + private List