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
-
-
- 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