From 2ba46e85c5b3c426d8a4288ea849e7f0b0c9e9b2 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sat, 6 Sep 2025 23:07:49 +0900 Subject: [PATCH 01/32] =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-example.properties | 0 src/main/resources/application.properties | 1 + 2 files changed, 1 insertion(+) create mode 100644 src/main/resources/application-example.properties create mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..16bdb5d --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=backend \ No newline at end of file From 9ca47820b1d36fc2aa6b05457018d8f176711af7 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sat, 6 Sep 2025 23:22:05 +0900 Subject: [PATCH 02/32] Ignore application.properties --- src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 16bdb5d..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=backend \ No newline at end of file From 2185d3124e363dac66367756d6213095116278e7 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sat, 6 Sep 2025 23:38:32 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a0b9bb7 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=backend1 \ No newline at end of file From 453975d9443accb9c1e1997c0adb5ee4f9985156 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sat, 6 Sep 2025 23:40:52 +0900 Subject: [PATCH 04/32] Ignore application.properties --- src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index a0b9bb7..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=backend1 \ No newline at end of file From 788e58ab0ccaddcf8a7269c51244b6642dcde62a Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sat, 6 Sep 2025 23:52:20 +0900 Subject: [PATCH 05/32] Update .gitignore --- .gitignore | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d6709aa..db44e26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +### Project Files ### HELP.md + +### Gradle ### .gradle build/ !gradle/wrapper/gradle-wrapper.jar @@ -28,12 +31,26 @@ out/ ### NetBeans ### /nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ ### VS Code ### .vscode/ -*.yml \ No newline at end of file +### OS Files ### +.DS_Store +Thumbs.db + +### Logs ### +*.log +logs/ + +### Environment & Config Files ### +.env +application.properties +application.yml + +### Others ### +*.class \ No newline at end of file From ec4d5d1fcd09317538f64e43c831aeb06e0d30b1 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Sun, 7 Sep 2025 00:03:50 +0900 Subject: [PATCH 06/32] Update application-example.properties --- src/main/resources/application-example.properties | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties index e69de29..6592aee 100644 --- a/src/main/resources/application-example.properties +++ b/src/main/resources/application-example.properties @@ -0,0 +1,12 @@ +spring.application.name=backend + +# MySQL +spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?serverTimezone=Asia/Seoul&useSSL=false +spring.datasource.username=your_username +spring.datasource.password=your_password +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect \ No newline at end of file From 58d60ca82dab3dc8f37db607cba5b202e5a93663 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Tue, 23 Sep 2025 17:44:05 +0900 Subject: [PATCH 07/32] =?UTF-8?q?feat:=20MultipartFile=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-example.properties | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties index 6592aee..f2d5f3e 100644 --- a/src/main/resources/application-example.properties +++ b/src/main/resources/application-example.properties @@ -1,7 +1,7 @@ spring.application.name=backend # MySQL -spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?serverTimezone=Asia/Seoul&useSSL=false +spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false spring.datasource.username=your_username spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver @@ -9,4 +9,10 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect + +# File Upload Settings +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB +file.upload-dir=uploads/profile \ No newline at end of file From 308390fd0a01d5c5af63fe9444bb7e51508ac3be Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Fri, 31 Oct 2025 15:11:01 +0900 Subject: [PATCH 08/32] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20/=20build.gradle=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- .../project/backend/config/WebConfig.java | 14 + .../controller/UserInfoController.java | 92 ++++++ .../project/backend/dto/BasicInfoDTO.java | 29 ++ .../project/backend/dto/DetailInfoDTO.java | 34 ++ .../backend/dto/UserInfoResponseDTO.java | 31 ++ .../project/backend/entity/BasicInfo.java | 49 +++ .../project/backend/entity/DetailInfo.java | 59 ++++ .../backend/kakaoLogin/AuthController.java | 23 ++ .../kakaoLogin/JwtAuthenticationFilter.java | 72 +++++ .../backend/kakaoLogin/JwtProperties.java | 16 + .../backend/kakaoLogin/JwtTokenProvider.java | 79 +++++ .../backend/kakaoLogin/JwtTokenResponse.java | 16 + .../backend/kakaoLogin/KakaoOAuthService.java | 133 ++++++++ .../backend/kakaoLogin/RefreshToken.java | 36 +++ .../kakaoLogin/RefreshTokenRepository.java | 9 + .../java/project/backend/kakaoLogin/Role.java | 6 + .../java/project/backend/kakaoLogin/User.java | 42 +++ .../backend/kakaoLogin/UserRepository.java | 10 + .../backend/kakaoLogin/WebSecurityConfig.java | 46 +++ .../backend/mypage/MyPageController.java | 99 ++++++ .../backend/mypage/MyPageDisplayDTO.java | 27 ++ .../project/backend/mypage/MyPageService.java | 143 +++++++++ .../backend/mypage/ProfileEditDTO.java | 36 +++ .../backend/mypage/ProfileUpdateDTO.java | 49 +++ .../repository/BasicInfoRepository.java | 13 + .../repository/DetailInfoRepository.java | 17 + .../backend/service/UserInfoService.java | 290 ++++++++++++++++++ 28 files changed, 1477 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/backend/config/WebConfig.java create mode 100644 src/main/java/project/backend/controller/UserInfoController.java create mode 100644 src/main/java/project/backend/dto/BasicInfoDTO.java create mode 100644 src/main/java/project/backend/dto/DetailInfoDTO.java create mode 100644 src/main/java/project/backend/dto/UserInfoResponseDTO.java create mode 100644 src/main/java/project/backend/entity/BasicInfo.java create mode 100644 src/main/java/project/backend/entity/DetailInfo.java create mode 100644 src/main/java/project/backend/kakaoLogin/AuthController.java create mode 100644 src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java create mode 100644 src/main/java/project/backend/kakaoLogin/JwtProperties.java create mode 100644 src/main/java/project/backend/kakaoLogin/JwtTokenProvider.java create mode 100644 src/main/java/project/backend/kakaoLogin/JwtTokenResponse.java create mode 100644 src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java create mode 100644 src/main/java/project/backend/kakaoLogin/RefreshToken.java create mode 100644 src/main/java/project/backend/kakaoLogin/RefreshTokenRepository.java create mode 100644 src/main/java/project/backend/kakaoLogin/Role.java create mode 100644 src/main/java/project/backend/kakaoLogin/User.java create mode 100644 src/main/java/project/backend/kakaoLogin/UserRepository.java create mode 100644 src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java create mode 100644 src/main/java/project/backend/mypage/MyPageController.java create mode 100644 src/main/java/project/backend/mypage/MyPageDisplayDTO.java create mode 100644 src/main/java/project/backend/mypage/MyPageService.java create mode 100644 src/main/java/project/backend/mypage/ProfileEditDTO.java create mode 100644 src/main/java/project/backend/mypage/ProfileUpdateDTO.java create mode 100644 src/main/java/project/backend/repository/BasicInfoRepository.java create mode 100644 src/main/java/project/backend/repository/DetailInfoRepository.java create mode 100644 src/main/java/project/backend/service/UserInfoService.java diff --git a/build.gradle b/build.gradle index 144ae94..6124480 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.5' + id 'org.springframework.boot' version '3.5.7' id 'io.spring.dependency-management' version '1.1.7' } @@ -31,11 +31,17 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } dependencyManagement { diff --git a/src/main/java/project/backend/config/WebConfig.java b/src/main/java/project/backend/config/WebConfig.java new file mode 100644 index 0000000..7f494ca --- /dev/null +++ b/src/main/java/project/backend/config/WebConfig.java @@ -0,0 +1,14 @@ +package project.backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/profile/**") + .addResourceLocations("file:uploads/profile/"); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/controller/UserInfoController.java b/src/main/java/project/backend/controller/UserInfoController.java new file mode 100644 index 0000000..6262c31 --- /dev/null +++ b/src/main/java/project/backend/controller/UserInfoController.java @@ -0,0 +1,92 @@ +package project.backend.controller; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.backend.dto.BasicInfoDTO; +import project.backend.dto.DetailInfoDTO; +import project.backend.dto.UserInfoResponseDTO; +import project.backend.service.UserInfoService; + +@RestController +@RequestMapping("/api/user-info") +@RequiredArgsConstructor +@Slf4j +public class UserInfoController { + private final UserInfoService userInfoService; + + // 기본 정보 저장 + @PostMapping("/basic") + public ResponseEntity saveBasicInfo(@Valid @RequestBody BasicInfoDTO basicInfoDTO) { + log.info("기본 정보 저장 요청: {}", basicInfoDTO.getName()); + BasicInfoDTO savedBasicInfo = userInfoService.saveBasicInfo(basicInfoDTO); + + return ResponseEntity.status(HttpStatus.CREATED).body(savedBasicInfo); + } + + // 상세 정보 저장(multipart/form-data) + @PostMapping(value = "/detail", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity saveDetailInfo( + @RequestParam("basicInfoId") Long basicInfoId, + @RequestParam(value = "profileImage", required = false) MultipartFile profileImage, + @RequestParam(value = "place", required = false) String place, + @RequestParam(value = "drinkingFrequency", required = false) String drinkingFrequency, + @RequestParam(value = "smokingStatus", required = false) String smokingStatus, + @RequestParam(value = "height", required = false) Integer height, + @RequestParam(value = "pet", required = false) String pet, + @RequestParam(value = "religion", required = false) String religion, + @RequestParam(value = "childPlan", required = false) String childPlan, + @RequestParam(value = "mbti", required = false) String mbti) { + + log.info("상세 정보 저장 요청 - 기본정보 ID: {}", basicInfoId); + + DetailInfoDTO detailInfoDTO = DetailInfoDTO.builder() + .basicInfoId(basicInfoId) + .profileImage(profileImage) + .place(place) + .drinkingFrequency(drinkingFrequency) + .smokingStatus(smokingStatus) + .height(height) + .pet(pet) + .religion(religion) + .childPlan(childPlan) + .mbti(mbti) + .build(); + + try { + DetailInfoDTO savedDetailInfo = userInfoService.saveDetailInfo(detailInfoDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(savedDetailInfo); + } catch(Exception e) { + log.error("상세 정보 저장 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // 테스트용 API + @GetMapping("/{userId}") + public ResponseEntity getUserInfo(@PathVariable("userId") Long userId) { + log.info("사용자 정보 조회 요청 - 사용자 ID: {}", userId); + UserInfoResponseDTO userInfo = userInfoService.getUserInfo(userId); + return ResponseEntity.ok(userInfo); + } + + @GetMapping("/same-place") + public ResponseEntity> getUsersBySamePlace(@RequestParam("basicInfoId") Long basicInfoId) { + List users = userInfoService.getUsersBySamePlace(basicInfoId); + return ResponseEntity.ok(users); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/dto/BasicInfoDTO.java b/src/main/java/project/backend/dto/BasicInfoDTO.java new file mode 100644 index 0000000..534d7b7 --- /dev/null +++ b/src/main/java/project/backend/dto/BasicInfoDTO.java @@ -0,0 +1,29 @@ +package project.backend.dto; + +import java.time.LocalDate; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BasicInfoDTO { + private Long id; + + @NotBlank(message = "이름은 필수 입력 값입니다.") + private String name; + + @NotBlank(message = "성별은 필수 입력 값입니다.") + private String gender; + + @NotNull(message = "생년월일은 필수 입력 값입니다.") + private LocalDate birthDate; +} \ No newline at end of file diff --git a/src/main/java/project/backend/dto/DetailInfoDTO.java b/src/main/java/project/backend/dto/DetailInfoDTO.java new file mode 100644 index 0000000..f10730f --- /dev/null +++ b/src/main/java/project/backend/dto/DetailInfoDTO.java @@ -0,0 +1,34 @@ +package project.backend.dto; + +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DetailInfoDTO { + private Long id; + private Long basicInfoId; + + @JsonIgnore + private MultipartFile profileImage; + + private String profileImagePath; + private String place; + private String drinkingFrequency; + private String smokingStatus; + private Integer height; + private String pet; + private String religion; + private String childPlan; + private String mbti; +} \ No newline at end of file diff --git a/src/main/java/project/backend/dto/UserInfoResponseDTO.java b/src/main/java/project/backend/dto/UserInfoResponseDTO.java new file mode 100644 index 0000000..5b4e94b --- /dev/null +++ b/src/main/java/project/backend/dto/UserInfoResponseDTO.java @@ -0,0 +1,31 @@ +package project.backend.dto; + +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserInfoResponseDTO { + private Long basicInfoId; + private String name; + private String gender; + private LocalDate birthDate; + private Long detailInfoId; + private String profileImagePath; + private String place; + private String drinkingFrequency; + private String smokingStatus; + private Integer height; + private String pet; + private String religion; + private String childPlan; + private String mbti; +} \ No newline at end of file diff --git a/src/main/java/project/backend/entity/BasicInfo.java b/src/main/java/project/backend/entity/BasicInfo.java new file mode 100644 index 0000000..46027c2 --- /dev/null +++ b/src/main/java/project/backend/entity/BasicInfo.java @@ -0,0 +1,49 @@ +package project.backend.entity; + +import java.time.LocalDate; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import project.backend.kakaoLogin.User; + +@Entity +@Table(name = "basic_info") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BasicInfo { + @Id // 기본키 필드 + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) // 기본 정보 - 이름 + private String name; + + @Column(nullable = false) // 기본 정보 - 성별 + private String gender; // 남자 또는 여자 + + @Column(name = "birth_date", nullable = false) + private LocalDate birthDate; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + @OneToOne(mappedBy = "basicInfo", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private DetailInfo detailInfo; +} \ No newline at end of file diff --git a/src/main/java/project/backend/entity/DetailInfo.java b/src/main/java/project/backend/entity/DetailInfo.java new file mode 100644 index 0000000..38e8fb4 --- /dev/null +++ b/src/main/java/project/backend/entity/DetailInfo.java @@ -0,0 +1,59 @@ +package project.backend.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "detail_info") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DetailInfo { + @Id // 기본키 필드 + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "basic_info_id", nullable = false) + private BasicInfo basicInfo; + + @Column(name = "profile_image_path") // 상세 정보 - 프로필 사진 + private String profileImagePath; + + @Column(name = "place") // 상세 정보 - 거주 지역 + private String place; // 예: 서울특별시, 경기도 용인시 + + @Column(name = "drinking_frequency") // 상세 정보 - 음주빈도 + private String drinkingFrequency; // 안 마심, 가끔 마심, 적당히 마심, 자주 마심 + + @Column(name = "smoking_status") // 상세 정보 - 흡연여부 + private String smokingStatus; // 비흡연, 가끔 흡연, 흡연 + + @Column(name = "height") // 상세 정보 - 키 + private Integer height; + + @Column(name = "pet") // 상세 정보 - 반려동물 + private String pet; // 없음, 강아지, 고양이, 기타 + + @Column(name = "religion") // 상세 정보 - 종교 + private String religion; // 무교, 불교, 기독교, 천주교, 기타 + + @Column(name = "child_plan") // 상세 정보 - 자녀계획 + private String childPlan; // 원함, 상관없음, 원하지 않음 + + @Column(name = "mbti", length = 4) // 상세 정보 - MBTI + private String mbti; +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/AuthController.java b/src/main/java/project/backend/kakaoLogin/AuthController.java new file mode 100644 index 0000000..276dd62 --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/AuthController.java @@ -0,0 +1,23 @@ +package project.backend.kakaoLogin; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final KakaoOAuthService kakaoOAuthService; + + @GetMapping("/kakao/callback") + public ResponseEntity kakaoCallback(@RequestParam("code") String code) { + JwtTokenResponse tokens = kakaoOAuthService.loginWithKakao(code); + return ResponseEntity.ok(tokens); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java b/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java new file mode 100644 index 0000000..68959f0 --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java @@ -0,0 +1,72 @@ +package project.backend.kakaoLogin; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + // Authorization 헤더에서 JWT 추출 + String header = request.getHeader("Authorization"); + if (header == null || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = header.substring(7); + + // 토큰 검증 + if (!jwtTokenProvider.validateToken(token)) { + filterChain.doFilter(request, response); + return; + } + + // 토큰에서 사용자 식별 정보(email 또는 kakaoId) 추출 + Claims claims = jwtTokenProvider.getClaims(token); + String subject = claims.getSubject(); + + if (subject == null) { + filterChain.doFilter(request, response); + return; + } + + // DB에서 사용자 조회 + User user = userRepository.findByEmail(subject) + .orElseGet(() -> userRepository.findByKakaoId(subject).orElse(null)); + + if (user == null) { + filterChain.doFilter(request, response); + return; + } + + // 인증 객체 생성 및 SecurityContext에 등록 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(user, null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/JwtProperties.java b/src/main/java/project/backend/kakaoLogin/JwtProperties.java new file mode 100644 index 0000000..5d3d3ef --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/JwtProperties.java @@ -0,0 +1,16 @@ +package project.backend.kakaoLogin; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Component +@ConfigurationProperties("jwt") +public class JwtProperties { + private String issuer; + private String secretKey; +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/JwtTokenProvider.java b/src/main/java/project/backend/kakaoLogin/JwtTokenProvider.java new file mode 100644 index 0000000..0e8b58e --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/JwtTokenProvider.java @@ -0,0 +1,79 @@ +package project.backend.kakaoLogin; + +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.Base64Codec; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret-key}") + private String secretKey; + + // 유효기간 설정 + private final long accessTokenValidity = 60 * 60 * 1000L; // 1시간 + private final long refreshTokenValidity = 7 * 24 * 60 * 60 * 1000L; // 7일 + + // Access Token 생성 + public String createAccessToken(String userId, String role) { + return createToken(userId, role, accessTokenValidity); + } + + // Refresh Token 생성 + public String createRefreshToken(String userId, String role) { + return createToken(userId, role, refreshTokenValidity); + } + + // 공통 토큰 생성 로직 + private String createToken(String userId, String role, long validity) { + Claims claims = Jwts.claims().setSubject(userId); + claims.put("role", role); + + Date now = new Date(); + Date expiration = new Date(now.getTime() + validity); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) + .compact(); + } + + // 토큰에서 사용자 ID 추출 + public String getUserIdFromToken(String token) { + return Jwts.parser() + .setSigningKey(secretKey.getBytes()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + // 토큰 유효성 검증 + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.warn("JWT 검증 실패: {}", e.getMessage()); + return false; + } + } + + // Claims 추출 + public Claims getClaims(String token) { + return Jwts.parser() + .setSigningKey(Base64Codec.BASE64.encode(secretKey)) + .parseClaimsJws(token) + .getBody(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/JwtTokenResponse.java b/src/main/java/project/backend/kakaoLogin/JwtTokenResponse.java new file mode 100644 index 0000000..07ac27c --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/JwtTokenResponse.java @@ -0,0 +1,16 @@ +package project.backend.kakaoLogin; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class JwtTokenResponse { + private String accessToken; + private String refreshToken; + private boolean isNewUser; // 기본 정보 없음 -> 신규 가입자 +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java b/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java new file mode 100644 index 0000000..2c003b8 --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java @@ -0,0 +1,133 @@ +package project.backend.kakaoLogin; + +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class KakaoOAuthService { + + private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String redirectUri; + + @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") + private String tokenUri; + + @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") + private String userInfoUri; + + public JwtTokenResponse loginWithKakao(String code) { + // access token 요청 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("client_id", clientId); + body.add("redirect_uri", redirectUri); + body.add("code", code); + + HttpEntity> tokenRequest = new HttpEntity<>(body, headers); + ResponseEntity tokenResponse = restTemplate.exchange(tokenUri, HttpMethod.POST, tokenRequest, String.class); + + Map tokenMap; + try { + tokenMap = objectMapper.readValue(tokenResponse.getBody(), new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("카카오 토큰 요청 실패", e); + } + + String kakaoAccessToken = (String) tokenMap.get("access_token"); + + // 사용자 정보 요청 + HttpHeaders userHeaders = new HttpHeaders(); + userHeaders.setBearerAuth(kakaoAccessToken); + HttpEntity userInfoReq = new HttpEntity<>(userHeaders); + + ResponseEntity userInfoResp = restTemplate.exchange(userInfoUri, HttpMethod.GET, userInfoReq, String.class); + + Map userMap; + try { + userMap = objectMapper.readValue(userInfoResp.getBody(), new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("카카오 사용자 정보 조회 실패", e); + } + + // 사용자 정보 파싱 + String kakaoId = String.valueOf(userMap.get("id")); + Map kakaoAccount = (Map) userMap.get("kakao_account"); + String email = kakaoAccount != null ? (String) kakaoAccount.get("email") : null; + + // DB 사용자 확인/저장 + Optional existingUser = userRepository.findByKakaoId(kakaoId); + boolean isNewUser = existingUser.isEmpty(); + + User user = existingUser.orElseGet(() -> { + User newUser = User.builder() + .kakaoId(kakaoId) + .email(email) + .role(Role.USER) + .build(); + return userRepository.save(newUser); + }); + + // JWT 발급 + String accessToken = jwtTokenProvider.createAccessToken( + (user.getEmail() != null) ? user.getEmail() : user.getKakaoId(), + user.getRole().name() + ); + + String refreshToken = jwtTokenProvider.createRefreshToken( + (user.getEmail() != null) ? user.getEmail() : user.getKakaoId(), + user.getRole().name() + ); + + // RefreshToken 저장 (기존 있으면 갱신) + refreshTokenRepository.findByUserId(user.getKakaoId()) + .ifPresentOrElse(existing -> { + existing.setToken(refreshToken); + refreshTokenRepository.save(existing); + }, () -> { + refreshTokenRepository.save( + RefreshToken.builder() + .userId(user.getKakaoId()) + .token(refreshToken) + .build() + ); + }); + + // 반환 + JwtTokenResponse resp = new JwtTokenResponse(); + resp.setAccessToken(accessToken); + resp.setRefreshToken(refreshToken); + resp.setNewUser(isNewUser); + + return resp; + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/RefreshToken.java b/src/main/java/project/backend/kakaoLogin/RefreshToken.java new file mode 100644 index 0000000..9c2604e --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/RefreshToken.java @@ -0,0 +1,36 @@ +package project.backend.kakaoLogin; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String userId; // user의 kakaoId 또는 email + + @Column(nullable = false, length = 500) + private String token; + + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/RefreshTokenRepository.java b/src/main/java/project/backend/kakaoLogin/RefreshTokenRepository.java new file mode 100644 index 0000000..906a7cf --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package project.backend.kakaoLogin; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByUserId(String userId); +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/Role.java b/src/main/java/project/backend/kakaoLogin/Role.java new file mode 100644 index 0000000..8d5e6b6 --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/Role.java @@ -0,0 +1,6 @@ +package project.backend.kakaoLogin; + +public enum Role { + USER, + ADMIN +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/User.java b/src/main/java/project/backend/kakaoLogin/User.java new file mode 100644 index 0000000..fd2366c --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/User.java @@ -0,0 +1,42 @@ +package project.backend.kakaoLogin; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import project.backend.entity.BasicInfo; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String kakaoId; + + @Column(unique = true) + private String email; + + @Enumerated(EnumType.STRING) + @Builder.Default + private Role role = Role.USER; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) + private BasicInfo basicInfo; +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/UserRepository.java b/src/main/java/project/backend/kakaoLogin/UserRepository.java new file mode 100644 index 0000000..209007c --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/UserRepository.java @@ -0,0 +1,10 @@ +package project.backend.kakaoLogin; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + Optional findByKakaoId(String kakaoId); + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java b/src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java new file mode 100644 index 0000000..ac9b140 --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java @@ -0,0 +1,46 @@ +package project.backend.kakaoLogin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class WebSecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // csrf 비활성화 + .csrf(csrf -> csrf.disable()) + // CORS 설정 허용 + .cors(Customizer.withDefaults()) + // 요청별 인가 규칙 + .authorizeHttpRequests(auth -> auth + // 테스트를 위해 "/api/**"를 추가함, 추후 보안 고려 시 세밀하게 관리하도록 수정하는 게 좋음 + .requestMatchers("/auth/**", "/login/**", "/api/**", "/oauth2/**").permitAll() + .anyRequest().authenticated() + ) + // OAuth2 로그인 설정 + .oauth2Login(oauth -> oauth + .defaultSuccessUrl("/auth/kakao/callback", true) + ) + // 세션 비활성화 (JWT 사용 시) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/MyPageController.java b/src/main/java/project/backend/mypage/MyPageController.java new file mode 100644 index 0000000..9cf24a3 --- /dev/null +++ b/src/main/java/project/backend/mypage/MyPageController.java @@ -0,0 +1,99 @@ +package project.backend.mypage; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/api/my-page") +@RequiredArgsConstructor +@Slf4j +public class MyPageController { + private final MyPageService myPageService; + + // 마이페이지 정보 조회 + @GetMapping("/{userId}") + public ResponseEntity getMyPageInfo(@PathVariable("userId") Long userId) { + log.info("마이페이지 조회 요청 - 사용자 ID: {}", userId); + MyPageDisplayDTO myPageInfo = myPageService.getMyPageInfo(userId); + + return ResponseEntity.ok(myPageInfo); + } + + // 프로필 수정 화면용 전체 정보 조회 + @GetMapping("/profile/{userId}") + public ResponseEntity getProfileForEdit(@PathVariable("userId") Long userId) { + log.info("프로필 수정 화면 정보 요청 - 사용자 ID: {}", userId); + ProfileEditDTO profileEdit = myPageService.getProfileForEdit(userId); + + return ResponseEntity.ok(profileEdit); + } + + // 프로필 전체 수정 (기본 정보 + 상세 정보, 이미지 포함) + @PutMapping(value = "/profile/{userId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity updateProfile( + @PathVariable(value = "userId") Long userId, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "gender", required = false) String gender, + @RequestParam(value = "birthDate", required = false) String birthDate, + @RequestParam(value = "profileImage", required = false) MultipartFile profileImage, + @RequestParam(value = "place", required = false) String place, + @RequestParam(value = "drinkingFrequency", required = false) String drinkingFrequency, + @RequestParam(value = "smokingStatus", required = false) String smokingStatus, + @RequestParam(value = "height", required = false) Integer height, + @RequestParam(value = "pet", required = false) String pet, + @RequestParam(value = "religion", required = false) String religion, + @RequestParam(value = "childPlan", required = false) String childPlan, + @RequestParam(value = "mbti", required = false) String mbti + ) throws IOException { + + log.info("프로필 수정 요청 - 사용자 ID: {}", userId); + + ProfileUpdateDTO updateDTO = ProfileUpdateDTO.builder() + .name(name) + .gender(gender) + .birthDate((birthDate != null && !birthDate.isEmpty()) ? LocalDate.parse(birthDate) : null) + .profileImage(profileImage) + .place(place) + .drinkingFrequency(drinkingFrequency) + .smokingStatus(smokingStatus) + .height(height) + .pet(pet) + .religion(religion) + .childPlan(childPlan) + .mbti(mbti) + .build(); + + ProfileEditDTO updatedProfile = myPageService.updateProfile(userId, updateDTO); + + return ResponseEntity.ok(updatedProfile); + } + + // 회원 탈퇴 + @DeleteMapping("/{userId}") + public ResponseEntity> deleteUser(@PathVariable("userId") Long userId) { + log.info("회원 탈퇴 요청 - 사용자 ID: {}", userId); + myPageService.deleteUser(userId); + + Map response = new HashMap<>(); + response.put("message", "회원 탈퇴가 완료되었습니다."); // 회원 탈퇴 성공 여부 확인하려고 작성함 + response.put("userId", userId.toString()); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/MyPageDisplayDTO.java new file mode 100644 index 0000000..0c3ef47 --- /dev/null +++ b/src/main/java/project/backend/mypage/MyPageDisplayDTO.java @@ -0,0 +1,27 @@ +package project.backend.mypage; + +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MyPageDisplayDTO { + private Long userId; //basicInfoId + private String profileImagePath; // 프로필 사진 + private String name; // 이름 + private Integer age; // 만 나이 (계산값) + private String gender; // 성별 + // 여기에 직업 추가 + private String place; // 거주 지역 + private LocalDate birthDate; // 생년월일 + private String mbti; // MBTI + // 여기에 자기소개 추가 +} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/MyPageService.java b/src/main/java/project/backend/mypage/MyPageService.java new file mode 100644 index 0000000..a8c7257 --- /dev/null +++ b/src/main/java/project/backend/mypage/MyPageService.java @@ -0,0 +1,143 @@ +package project.backend.mypage; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.Period; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.backend.dto.UserInfoResponseDTO; +import project.backend.entity.BasicInfo; +import project.backend.entity.DetailInfo; +import project.backend.repository.BasicInfoRepository; +import project.backend.repository.DetailInfoRepository; +import project.backend.service.UserInfoService; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MyPageService { + private final UserInfoService userInfoService; + private final BasicInfoRepository basicInfoRepository; + private final DetailInfoRepository detailInfoRepository; + + // 마이페이지 조회 + public MyPageDisplayDTO getMyPageInfo(Long userId) { + UserInfoResponseDTO userInfo = userInfoService.getUserInfo(userId); + + Integer age = calculateAge(userInfo.getBirthDate()); + + return MyPageDisplayDTO.builder() + .userId(userInfo.getBasicInfoId()) + .profileImagePath(userInfo.getProfileImagePath()) + .name(userInfo.getName()) + .age(age) + .gender(userInfo.getGender()) + .place(userInfo.getPlace()) + .birthDate(userInfo.getBirthDate()) + .mbti(userInfo.getMbti()) + .build(); + } + + // 프로필 수정 화면용 전체 정보 조회 + @Transactional(readOnly = true) + public ProfileEditDTO getProfileForEdit(Long userId) { + BasicInfo basicInfo = basicInfoRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); + + DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) + .orElse(null); + + ProfileEditDTO.ProfileEditDTOBuilder builder = ProfileEditDTO.builder() + .basicInfoId(basicInfo.getId()) + .name(basicInfo.getName()) + .gender(basicInfo.getGender()) + .birthDate(basicInfo.getBirthDate()); + + if (detailInfo != null) { + builder.detailInfoId(detailInfo.getId()) + .profileImagePath(detailInfo.getProfileImagePath()) + .place(detailInfo.getPlace()) + .drinkingFrequency(detailInfo.getDrinkingFrequency()) + .smokingStatus(detailInfo.getSmokingStatus()) + .height(detailInfo.getHeight()) + .pet(detailInfo.getPet()) + .religion(detailInfo.getReligion()) + .childPlan(detailInfo.getChildPlan()) + .mbti(detailInfo.getMbti()); + } + + return builder.build(); + } + + // 프로필 전체 수정 (기본 정보 + 상세 정보) + @Transactional + public ProfileEditDTO updateProfile(Long userId, ProfileUpdateDTO updateDTO) throws IOException { + BasicInfo basicInfo = basicInfoRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); + + // 기본 정보 업데이트 (null 체크) + if (updateDTO.getName() != null) basicInfo.setName(updateDTO.getName()); + if (updateDTO.getGender() != null) basicInfo.setGender(updateDTO.getGender()); + if (updateDTO.getBirthDate() != null) basicInfo.setBirthDate(updateDTO.getBirthDate()); + + BasicInfo savedBasicInfo = basicInfoRepository.save(basicInfo); + + // 상세 정보 + DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) + .orElse(DetailInfo.builder() + .basicInfo(savedBasicInfo) + .build()); + + // 프로필 이미지 처리 + if (updateDTO.getProfileImage() != null && !updateDTO.getProfileImage().isEmpty()) { + String newProfileImagePath = userInfoService.updateProfileImage(userId, updateDTO.getProfileImage()); + detailInfo.setProfileImagePath(newProfileImagePath); + } + + // null 체크 후 업데이트 + if (updateDTO.getPlace() != null) detailInfo.setPlace(updateDTO.getPlace()); + if (updateDTO.getDrinkingFrequency() != null) detailInfo.setDrinkingFrequency(updateDTO.getDrinkingFrequency()); + if (updateDTO.getSmokingStatus() != null) detailInfo.setSmokingStatus(updateDTO.getSmokingStatus()); + if (updateDTO.getHeight() != null) detailInfo.setHeight(updateDTO.getHeight()); + if (updateDTO.getPet() != null) detailInfo.setPet(updateDTO.getPet()); + if (updateDTO.getReligion() != null) detailInfo.setReligion(updateDTO.getReligion()); + if (updateDTO.getChildPlan() != null) detailInfo.setChildPlan(updateDTO.getChildPlan()); + if (updateDTO.getMbti() != null) detailInfo.setMbti(updateDTO.getMbti()); + + DetailInfo savedDetailInfo = detailInfoRepository.save(detailInfo); + + // 응답 DTO 생성 (수정 완료 후 전체 사용자 정보 반환) + return ProfileEditDTO.builder() + .basicInfoId(savedBasicInfo.getId()) + .name(savedBasicInfo.getName()) + .gender(savedBasicInfo.getGender()) + .birthDate(savedBasicInfo.getBirthDate()) + .detailInfoId(savedDetailInfo.getId()) + .profileImagePath(savedDetailInfo.getProfileImagePath()) + .place(savedDetailInfo.getPlace()) + .drinkingFrequency(savedDetailInfo.getDrinkingFrequency()) + .smokingStatus(savedDetailInfo.getSmokingStatus()) + .height(savedDetailInfo.getHeight()) + .pet(savedDetailInfo.getPet()) + .religion(savedDetailInfo.getReligion()) + .childPlan(savedDetailInfo.getChildPlan()) + .mbti(savedDetailInfo.getMbti()) + .build(); + } + + // 회원 탈퇴 + public void deleteUser(Long userId) { + userInfoService.deleteUser(userId); + log.info("회원 탈퇴 완료 - 사용자 ID: {}", userId); + } + + // 나이 계산 (만 나이) + private Integer calculateAge(LocalDate birthDate) { + return (birthDate == null) ? null : Period.between(birthDate, LocalDate.now()).getYears(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/ProfileEditDTO.java b/src/main/java/project/backend/mypage/ProfileEditDTO.java new file mode 100644 index 0000000..e771dfc --- /dev/null +++ b/src/main/java/project/backend/mypage/ProfileEditDTO.java @@ -0,0 +1,36 @@ +// 프로필 수정 화면용 - 전체 정보 + +package project.backend.mypage; + +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProfileEditDTO { + // 기본 정보 + private Long basicInfoId; + private String name; + private String gender; + private LocalDate birthDate; + + // 상세 정보 + private Long detailInfoId; + private String profileImagePath; + private String place; + private String drinkingFrequency; + private String smokingStatus; + private Integer height; + private String pet; + private String religion; + private String childPlan; + private String mbti; +} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/ProfileUpdateDTO.java b/src/main/java/project/backend/mypage/ProfileUpdateDTO.java new file mode 100644 index 0000000..713bd8e --- /dev/null +++ b/src/main/java/project/backend/mypage/ProfileUpdateDTO.java @@ -0,0 +1,49 @@ +// 프로필 수정 요청용 + +package project.backend.mypage; + +import java.time.LocalDate; + +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProfileUpdateDTO { + // 기본 정보 + @NotBlank(message = "이름은 필수 입력 값입니다.") + private String name; + + @NotBlank(message = "성별은 필수 입력 값입니다.") + private String gender; + + @NotBlank(message = "생년월일은 필수 입력 값입니다.") + private LocalDate birthDate; + + // 상세 정보 + private Long detailInfoId; + + @JsonIgnore + private MultipartFile profileImage; + + private String place; + private String drinkingFrequency; + private String smokingStatus; + private Integer height; + private String pet; + private String religion; + private String childPlan; + private String mbti; + +} \ No newline at end of file diff --git a/src/main/java/project/backend/repository/BasicInfoRepository.java b/src/main/java/project/backend/repository/BasicInfoRepository.java new file mode 100644 index 0000000..18a83d8 --- /dev/null +++ b/src/main/java/project/backend/repository/BasicInfoRepository.java @@ -0,0 +1,13 @@ +package project.backend.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import project.backend.entity.BasicInfo; + +@Repository +public interface BasicInfoRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/project/backend/repository/DetailInfoRepository.java b/src/main/java/project/backend/repository/DetailInfoRepository.java new file mode 100644 index 0000000..c81f36a --- /dev/null +++ b/src/main/java/project/backend/repository/DetailInfoRepository.java @@ -0,0 +1,17 @@ +package project.backend.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import project.backend.entity.DetailInfo; + +@Repository +public interface DetailInfoRepository extends JpaRepository { + Optional findByBasicInfoId(Long basicInfoId); + + // 같은 거주 지역(place)인 사용자들 목록 + List findByPlace(String place); +} \ No newline at end of file diff --git a/src/main/java/project/backend/service/UserInfoService.java b/src/main/java/project/backend/service/UserInfoService.java new file mode 100644 index 0000000..e1188b7 --- /dev/null +++ b/src/main/java/project/backend/service/UserInfoService.java @@ -0,0 +1,290 @@ +package project.backend.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.backend.dto.BasicInfoDTO; +import project.backend.dto.DetailInfoDTO; +import project.backend.dto.UserInfoResponseDTO; +import project.backend.entity.BasicInfo; +import project.backend.entity.DetailInfo; +import project.backend.repository.BasicInfoRepository; +import project.backend.repository.DetailInfoRepository; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional +public class UserInfoService { + + private final BasicInfoRepository basicInfoRepository; + private final DetailInfoRepository detailInfoRepository; + + @Value("${file.upload-dir:uploads/profile}") + private String uploadDir; + + // 기본 정보 저장 + public BasicInfoDTO saveBasicInfo(BasicInfoDTO basicInfoDTO) { + BasicInfo basicInfo = BasicInfo.builder() + .name(basicInfoDTO.getName()) + .gender(basicInfoDTO.getGender()) + .birthDate(basicInfoDTO.getBirthDate()) + .build(); + + BasicInfo savedBasicInfo = basicInfoRepository.save(basicInfo); + + return BasicInfoDTO.builder() + .id(savedBasicInfo.getId()) + .name(savedBasicInfo.getName()) + .gender(savedBasicInfo.getGender()) + .birthDate(savedBasicInfo.getBirthDate()) + .build(); + } + + // 상세 정보 저장 + public DetailInfoDTO saveDetailInfo(DetailInfoDTO detailInfoDTO) throws IOException { + // 기본 정보 조회 + BasicInfo basicInfo = basicInfoRepository.findById(detailInfoDTO.getBasicInfoId()) + .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + detailInfoDTO.getBasicInfoId())); + + // 프로필 이미지 처리 + String profileImagePath = null; + if (detailInfoDTO.getProfileImage() != null && !detailInfoDTO.getProfileImage().isEmpty()) { + profileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); + } + + DetailInfo detailInfo = DetailInfo.builder() + .basicInfo(basicInfo) + .profileImagePath(profileImagePath) + .place(detailInfoDTO.getPlace()) + .drinkingFrequency(detailInfoDTO.getDrinkingFrequency()) + .smokingStatus(detailInfoDTO.getSmokingStatus()) + .height(detailInfoDTO.getHeight()) + .pet(detailInfoDTO.getPet()) + .religion(detailInfoDTO.getReligion()) + .childPlan(detailInfoDTO.getChildPlan()) + .mbti(detailInfoDTO.getMbti()) + .build(); + + DetailInfo savedDetailInfo = detailInfoRepository.save(detailInfo); + + return DetailInfoDTO.builder() + .id(savedDetailInfo.getId()) + .basicInfoId(savedDetailInfo.getBasicInfo().getId()) + .profileImagePath(savedDetailInfo.getProfileImagePath()) + .place(savedDetailInfo.getPlace()) + .drinkingFrequency(savedDetailInfo.getDrinkingFrequency()) + .smokingStatus(savedDetailInfo.getSmokingStatus()) + .height(savedDetailInfo.getHeight()) + .pet(savedDetailInfo.getPet()) + .religion(savedDetailInfo.getReligion()) + .childPlan(savedDetailInfo.getChildPlan()) + .mbti(savedDetailInfo.getMbti()) + .build(); + } + + // 전체 사용자 정보 조회 + @Transactional(readOnly = true) + public UserInfoResponseDTO getUserInfo(Long basicInfoId) { + BasicInfo basicInfo = basicInfoRepository.findById(basicInfoId) + .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + basicInfoId)); + + DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(basicInfoId) + .orElse(null); + + UserInfoResponseDTO.UserInfoResponseDTOBuilder builder = UserInfoResponseDTO.builder() + .basicInfoId(basicInfo.getId()) + .name(basicInfo.getName()) + .gender(basicInfo.getGender()) + .birthDate(basicInfo.getBirthDate()); + + if (detailInfo != null) { + builder.detailInfoId(basicInfo.getId()) + .profileImagePath(detailInfo.getProfileImagePath()) + .place(detailInfo.getPlace()) + .drinkingFrequency(detailInfo.getDrinkingFrequency()) + .smokingStatus(detailInfo.getSmokingStatus()) + .height(detailInfo.getHeight()) + .pet(detailInfo.getPet()) + .religion(detailInfo.getReligion()) + .childPlan(detailInfo.getChildPlan()) + .mbti(detailInfo.getMbti()); + } + + return builder.build(); + } + + // 기본 정보 수정 + public BasicInfoDTO updateBasicInfo(Long id, BasicInfoDTO basicInfoDTO) { + BasicInfo basicInfo = basicInfoRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + id)); + + basicInfo.setName(basicInfoDTO.getName()); + basicInfo.setGender(basicInfoDTO.getGender()); + basicInfo.setBirthDate(basicInfoDTO.getBirthDate()); + + BasicInfo updatedBasicInfo = basicInfoRepository.save(basicInfo); + + return BasicInfoDTO.builder() + .id(updatedBasicInfo.getId()) + .name(updatedBasicInfo.getName()) + .gender(updatedBasicInfo.getGender()) + .birthDate(updatedBasicInfo.getBirthDate()) + .build(); + } + + // 상세 정보 수정 + public DetailInfoDTO updateDetailInfo(Long id, DetailInfoDTO detailInfoDTO) throws IOException { + DetailInfo detailInfo = detailInfoRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + id)); + + // 새로운 프로필 이미지가 있으면 처리 + if (detailInfoDTO.getProfileImage() != null && !detailInfoDTO.getProfileImage().isEmpty()) { + // 기존 이미지 삭제(선택사항) + if (detailInfo.getProfileImagePath() != null) { + deleteProfileImage(detailInfo.getProfileImagePath()); + } + String newProfileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); + detailInfo.setProfileImagePath(newProfileImagePath); + } + + detailInfo.setPlace(detailInfoDTO.getPlace()); + detailInfo.setDrinkingFrequency(detailInfoDTO.getDrinkingFrequency()); + detailInfo.setSmokingStatus(detailInfoDTO.getSmokingStatus()); + detailInfo.setHeight(detailInfoDTO.getHeight()); + detailInfo.setPet(detailInfoDTO.getPet()); + detailInfo.setReligion(detailInfoDTO.getReligion()); + detailInfo.setChildPlan(detailInfoDTO.getChildPlan()); + detailInfo.setMbti(detailInfoDTO.getMbti()); + + DetailInfo updatedDetailInfo = detailInfoRepository.save(detailInfo); + + return DetailInfoDTO.builder() + .id(updatedDetailInfo.getId()) + .place(updatedDetailInfo.getPlace()) + .drinkingFrequency(updatedDetailInfo.getDrinkingFrequency()) + .smokingStatus(updatedDetailInfo.getSmokingStatus()) + .height(updatedDetailInfo.getHeight()) + .pet(updatedDetailInfo.getPet()) + .religion(updatedDetailInfo.getReligion()) + .childPlan(updatedDetailInfo.getChildPlan()) + .mbti(updatedDetailInfo.getMbti()) + .build(); + } + + // 현재 사용자의 거주 지역을 기준으로 같은 지역 사용자 찾기 + @Transactional(readOnly = true) + public List getUsersBySamePlace(Long myBasicInfoId) { + // 내 상세정보 가져오기 + DetailInfo myDetail = detailInfoRepository.findByBasicInfoId(myBasicInfoId) + .orElseThrow(() -> new IllegalArgumentException("상세정보를 찾을 수 없습니다.")); + + String myPlace = myDetail.getPlace(); + + // 같은 지역 사용자 전체 조회 + List usersInSamePlace = detailInfoRepository.findByPlace(myPlace); + + return usersInSamePlace.stream() + .filter(detail -> !detail.getBasicInfo().getId().equals(myBasicInfoId)) // 자신 제외 + .map(detail -> { + BasicInfo basic = detail.getBasicInfo(); + + return UserInfoResponseDTO.builder() + .basicInfoId(basic.getId()) + .name(basic.getName()) + .gender(basic.getGender()) + .birthDate(basic.getBirthDate()) + .detailInfoId(detail.getId()) + .profileImagePath(detail.getProfileImagePath()) + .place(detail.getPlace()) + .drinkingFrequency(detail.getDrinkingFrequency()) + .smokingStatus(detail.getSmokingStatus()) + .height(detail.getHeight()) + .pet(detail.getPet()) + .religion(detail.getReligion()) + .childPlan(detail.getChildPlan()) + .mbti(detail.getMbti()) + .build(); + }) + .toList(); + } + + // 회원 탈퇴 (MyPageService에서 위임 호출) + public void deleteUser(Long userId) { + BasicInfo basicInfo = basicInfoRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); + + // 프로필 이미지 삭제 + DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId).orElse(null); + if (detailInfo != null && detailInfo.getProfileImagePath() != null) { + deleteProfileImage(detailInfo.getProfileImagePath()); + } + + // DB에서 삭제 (Cascade 설정으로 DetailInfo도 함께 삭제됨) + basicInfoRepository.delete(basicInfo); + + log.info("회원 탈퇴 완료 - 사용자 ID: {}", userId); + } + + // 프로필 이미지 저장 + private String saveProfileImage(MultipartFile file) throws IOException { + // 업로드 디렉터리 생성 + Path uploadPath = Paths.get(uploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // 고유한 파일명 생성 + String originalFilename = file.getOriginalFilename(); + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String fileName = UUID.randomUUID().toString() + extension; + + // 파일 저장 + Path filePath = uploadPath.resolve(fileName); + Files.copy(file.getInputStream(), filePath); + + return "/uploads/profile/" + fileName; + } + + // 프로필 이미지 삭제 + private void deleteProfileImage(String imagePath) { + try { + String fileName = imagePath.substring(imagePath.lastIndexOf("/") + 1); + Path filePath = Paths.get(uploadDir).resolve(fileName); + Files.deleteIfExists(filePath); + } catch (Exception e) { + log.error("프로필 이미지 삭제 실패: " + imagePath, e); + } + } + + // MyPageService 위임 호출용 + // 기존 프로필 이미지를 삭제하고 새로운 프로필 이미지 저장 후 경로 반환 + public String updateProfileImage(Long userId, MultipartFile newImage) throws IOException { + DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) + .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + userId)); + + // 기존 이미지 삭제 + if (detailInfo.getProfileImagePath() != null) { + deleteProfileImage(detailInfo.getProfileImagePath()); + } + + // 새 이미지 저장 + String newPath = saveProfileImage(newImage); + detailInfo.setProfileImagePath(newPath); + detailInfoRepository.save(detailInfo); + + return newPath; + } +} \ No newline at end of file From e33312d4ead9ef50018ab07d76d838da561b57b7 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Fri, 31 Oct 2025 15:38:53 +0900 Subject: [PATCH 09/32] =?UTF-8?q?feat:=20JWT=20=EC=84=A4=EC=A0=95,=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAuth=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/application-example.properties | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties index f2d5f3e..5ec3e74 100644 --- a/src/main/resources/application-example.properties +++ b/src/main/resources/application-example.properties @@ -15,4 +15,19 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB -file.upload-dir=uploads/profile \ No newline at end of file +file.upload-dir=uploads/profile + +# JWT Settings +jwt.issuer=your_issuer +jwt.secret-key=your_secret_key + +# Kakao OAuth Settings +spring.security.oauth2.client.registration.kakao.client-id=your_client_id +spring.security.oauth2.client.registration.kakao.client-name=your_client_name +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/auth/kakao/callback +spring.security.oauth2.client.registration.kakao.scope=account_email +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id \ No newline at end of file From 3108c050633b9c4e5559494441fd4584293fcabf Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Fri, 31 Oct 2025 15:40:38 +0900 Subject: [PATCH 10/32] =?UTF-8?q?feat:=20JWT=20=EC=84=A4=EC=A0=95=20/=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAuth=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-example.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties index 5ec3e74..3814485 100644 --- a/src/main/resources/application-example.properties +++ b/src/main/resources/application-example.properties @@ -22,7 +22,7 @@ jwt.issuer=your_issuer jwt.secret-key=your_secret_key # Kakao OAuth Settings -spring.security.oauth2.client.registration.kakao.client-id=your_client_id +spring.security.oauth2.client.registration.kakao.client-id=your_client_i spring.security.oauth2.client.registration.kakao.client-name=your_client_name spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/auth/kakao/callback From 169bb6eb781ee61fd42bbd6a8d5e4187ac116476 Mon Sep 17 00:00:00 2001 From: lsryl13578 Date: Fri, 31 Oct 2025 15:42:17 +0900 Subject: [PATCH 11/32] =?UTF-8?q?feat:=20JWT=20=EC=84=A4=EC=A0=95=20/=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAuth=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-example.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties index 3814485..5ec3e74 100644 --- a/src/main/resources/application-example.properties +++ b/src/main/resources/application-example.properties @@ -22,7 +22,7 @@ jwt.issuer=your_issuer jwt.secret-key=your_secret_key # Kakao OAuth Settings -spring.security.oauth2.client.registration.kakao.client-id=your_client_i +spring.security.oauth2.client.registration.kakao.client-id=your_client_id spring.security.oauth2.client.registration.kakao.client-name=your_client_name spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/auth/kakao/callback From 6c3043ae1794d2652a9f4f1e8305ae015fa94a05 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 1 Nov 2025 16:40:18 +0900 Subject: [PATCH 12/32] =?UTF-8?q?refactor=20:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=8F=20gradle=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 23 +++++++++++++++---- .../project/backend/mypage/MyPageService.java | 12 +++++----- .../UserInfoController.java | 9 ++++---- .../{service => user}/UserInfoService.java | 16 ++++++------- .../backend/{config => user}/WebConfig.java | 2 +- .../backend/{ => user}/dto/BasicInfoDTO.java | 2 +- .../backend/{ => user}/dto/DetailInfoDTO.java | 2 +- .../{ => user}/dto/UserInfoResponseDTO.java | 2 +- .../backend/{ => user}/entity/BasicInfo.java | 2 +- .../backend/{ => user}/entity/DetailInfo.java | 2 +- .../repository/BasicInfoRepository.java | 4 ++-- .../repository/DetailInfoRepository.java | 4 ++-- 12 files changed, 46 insertions(+), 34 deletions(-) rename src/main/java/project/backend/{controller => user}/UserInfoController.java (94%) rename src/main/java/project/backend/{service => user}/UserInfoService.java (96%) rename src/main/java/project/backend/{config => user}/WebConfig.java (93%) rename src/main/java/project/backend/{ => user}/dto/BasicInfoDTO.java (94%) rename src/main/java/project/backend/{ => user}/dto/DetailInfoDTO.java (95%) rename src/main/java/project/backend/{ => user}/dto/UserInfoResponseDTO.java (94%) rename src/main/java/project/backend/{ => user}/entity/BasicInfo.java (97%) rename src/main/java/project/backend/{ => user}/entity/DetailInfo.java (98%) rename src/main/java/project/backend/{ => user}/repository/BasicInfoRepository.java (76%) rename src/main/java/project/backend/{ => user}/repository/DetailInfoRepository.java (82%) diff --git a/build.gradle b/build.gradle index 6124480..31f804f 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,12 @@ configurations { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } + maven { + name = 'Central Portal Snapshots' + url = 'https://central.sonatype.com/repository/maven-snapshots/' + } } ext { @@ -29,19 +35,26 @@ ext { } dependencies { + //공통 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'io.jsonwebtoken:jjwt:0.9.1' - implementation 'javax.xml.bind:jaxb-api:2.3.1' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - + + //상렬이거 + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + //내거 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT") + implementation 'org.springframework.ai:spring-ai-starter-model-openai' } dependencyManagement { diff --git a/src/main/java/project/backend/mypage/MyPageService.java b/src/main/java/project/backend/mypage/MyPageService.java index a8c7257..220f4d2 100644 --- a/src/main/java/project/backend/mypage/MyPageService.java +++ b/src/main/java/project/backend/mypage/MyPageService.java @@ -10,12 +10,12 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import project.backend.dto.UserInfoResponseDTO; -import project.backend.entity.BasicInfo; -import project.backend.entity.DetailInfo; -import project.backend.repository.BasicInfoRepository; -import project.backend.repository.DetailInfoRepository; -import project.backend.service.UserInfoService; +import project.backend.user.dto.UserInfoResponseDTO; +import project.backend.user.entity.BasicInfo; +import project.backend.user.entity.DetailInfo; +import project.backend.user.repository.BasicInfoRepository; +import project.backend.user.repository.DetailInfoRepository; +import project.backend.user.UserInfoService; @Service @RequiredArgsConstructor diff --git a/src/main/java/project/backend/controller/UserInfoController.java b/src/main/java/project/backend/user/UserInfoController.java similarity index 94% rename from src/main/java/project/backend/controller/UserInfoController.java rename to src/main/java/project/backend/user/UserInfoController.java index 6262c31..987921e 100644 --- a/src/main/java/project/backend/controller/UserInfoController.java +++ b/src/main/java/project/backend/user/UserInfoController.java @@ -1,4 +1,4 @@ -package project.backend.controller; +package project.backend.user; import java.util.List; @@ -17,10 +17,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import project.backend.dto.BasicInfoDTO; -import project.backend.dto.DetailInfoDTO; -import project.backend.dto.UserInfoResponseDTO; -import project.backend.service.UserInfoService; +import project.backend.user.dto.BasicInfoDTO; +import project.backend.user.dto.DetailInfoDTO; +import project.backend.user.dto.UserInfoResponseDTO; @RestController @RequestMapping("/api/user-info") diff --git a/src/main/java/project/backend/service/UserInfoService.java b/src/main/java/project/backend/user/UserInfoService.java similarity index 96% rename from src/main/java/project/backend/service/UserInfoService.java rename to src/main/java/project/backend/user/UserInfoService.java index e1188b7..6a89a09 100644 --- a/src/main/java/project/backend/service/UserInfoService.java +++ b/src/main/java/project/backend/user/UserInfoService.java @@ -1,4 +1,4 @@ -package project.backend.service; +package project.backend.user; import java.io.IOException; import java.nio.file.Files; @@ -15,13 +15,13 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import project.backend.dto.BasicInfoDTO; -import project.backend.dto.DetailInfoDTO; -import project.backend.dto.UserInfoResponseDTO; -import project.backend.entity.BasicInfo; -import project.backend.entity.DetailInfo; -import project.backend.repository.BasicInfoRepository; -import project.backend.repository.DetailInfoRepository; +import project.backend.user.dto.BasicInfoDTO; +import project.backend.user.dto.DetailInfoDTO; +import project.backend.user.dto.UserInfoResponseDTO; +import project.backend.user.entity.BasicInfo; +import project.backend.user.entity.DetailInfo; +import project.backend.user.repository.BasicInfoRepository; +import project.backend.user.repository.DetailInfoRepository; @Service @RequiredArgsConstructor diff --git a/src/main/java/project/backend/config/WebConfig.java b/src/main/java/project/backend/user/WebConfig.java similarity index 93% rename from src/main/java/project/backend/config/WebConfig.java rename to src/main/java/project/backend/user/WebConfig.java index 7f494ca..3497912 100644 --- a/src/main/java/project/backend/config/WebConfig.java +++ b/src/main/java/project/backend/user/WebConfig.java @@ -1,4 +1,4 @@ -package project.backend.config; +package project.backend.user; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; diff --git a/src/main/java/project/backend/dto/BasicInfoDTO.java b/src/main/java/project/backend/user/dto/BasicInfoDTO.java similarity index 94% rename from src/main/java/project/backend/dto/BasicInfoDTO.java rename to src/main/java/project/backend/user/dto/BasicInfoDTO.java index 534d7b7..417b562 100644 --- a/src/main/java/project/backend/dto/BasicInfoDTO.java +++ b/src/main/java/project/backend/user/dto/BasicInfoDTO.java @@ -1,4 +1,4 @@ -package project.backend.dto; +package project.backend.user.dto; import java.time.LocalDate; diff --git a/src/main/java/project/backend/dto/DetailInfoDTO.java b/src/main/java/project/backend/user/dto/DetailInfoDTO.java similarity index 95% rename from src/main/java/project/backend/dto/DetailInfoDTO.java rename to src/main/java/project/backend/user/dto/DetailInfoDTO.java index f10730f..8681fda 100644 --- a/src/main/java/project/backend/dto/DetailInfoDTO.java +++ b/src/main/java/project/backend/user/dto/DetailInfoDTO.java @@ -1,4 +1,4 @@ -package project.backend.dto; +package project.backend.user.dto; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/project/backend/dto/UserInfoResponseDTO.java b/src/main/java/project/backend/user/dto/UserInfoResponseDTO.java similarity index 94% rename from src/main/java/project/backend/dto/UserInfoResponseDTO.java rename to src/main/java/project/backend/user/dto/UserInfoResponseDTO.java index 5b4e94b..019b746 100644 --- a/src/main/java/project/backend/dto/UserInfoResponseDTO.java +++ b/src/main/java/project/backend/user/dto/UserInfoResponseDTO.java @@ -1,4 +1,4 @@ -package project.backend.dto; +package project.backend.user.dto; import java.time.LocalDate; diff --git a/src/main/java/project/backend/entity/BasicInfo.java b/src/main/java/project/backend/user/entity/BasicInfo.java similarity index 97% rename from src/main/java/project/backend/entity/BasicInfo.java rename to src/main/java/project/backend/user/entity/BasicInfo.java index 46027c2..baff165 100644 --- a/src/main/java/project/backend/entity/BasicInfo.java +++ b/src/main/java/project/backend/user/entity/BasicInfo.java @@ -1,4 +1,4 @@ -package project.backend.entity; +package project.backend.user.entity; import java.time.LocalDate; diff --git a/src/main/java/project/backend/entity/DetailInfo.java b/src/main/java/project/backend/user/entity/DetailInfo.java similarity index 98% rename from src/main/java/project/backend/entity/DetailInfo.java rename to src/main/java/project/backend/user/entity/DetailInfo.java index 38e8fb4..2542fa5 100644 --- a/src/main/java/project/backend/entity/DetailInfo.java +++ b/src/main/java/project/backend/user/entity/DetailInfo.java @@ -1,4 +1,4 @@ -package project.backend.entity; +package project.backend.user.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/project/backend/repository/BasicInfoRepository.java b/src/main/java/project/backend/user/repository/BasicInfoRepository.java similarity index 76% rename from src/main/java/project/backend/repository/BasicInfoRepository.java rename to src/main/java/project/backend/user/repository/BasicInfoRepository.java index 18a83d8..6c90cbc 100644 --- a/src/main/java/project/backend/repository/BasicInfoRepository.java +++ b/src/main/java/project/backend/user/repository/BasicInfoRepository.java @@ -1,11 +1,11 @@ -package project.backend.repository; +package project.backend.user.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import project.backend.entity.BasicInfo; +import project.backend.user.entity.BasicInfo; @Repository public interface BasicInfoRepository extends JpaRepository { diff --git a/src/main/java/project/backend/repository/DetailInfoRepository.java b/src/main/java/project/backend/user/repository/DetailInfoRepository.java similarity index 82% rename from src/main/java/project/backend/repository/DetailInfoRepository.java rename to src/main/java/project/backend/user/repository/DetailInfoRepository.java index c81f36a..63afa6a 100644 --- a/src/main/java/project/backend/repository/DetailInfoRepository.java +++ b/src/main/java/project/backend/user/repository/DetailInfoRepository.java @@ -1,4 +1,4 @@ -package project.backend.repository; +package project.backend.user.repository; import java.util.List; import java.util.Optional; @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import project.backend.entity.DetailInfo; +import project.backend.user.entity.DetailInfo; @Repository public interface DetailInfoRepository extends JpaRepository { From 88f7b212af1263ecb5cc14ba4f7b4837b6d9cacf Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 1 Nov 2025 20:46:37 +0900 Subject: [PATCH 13/32] =?UTF-8?q?refactor=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8B=9C=20=EC=A0=95=EB=B3=B4=20=ED=95=9C=20?= =?UTF-8?q?=EB=B2=88=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 회원가입시 정보 하나 하나 저장-> 기본 정보 상세 정보 등등 모두 전달 받은 후 한꺼번에 저장(사진은 아직 구현 x) --- .../backend/user/UserInfoController.java | 84 +------ .../project/backend/user/UserRepository.java | 13 ++ ...{UserInfoService.java => UserService.java} | 220 ++++++++++-------- .../backend/user/dto/BasicInfoDTO.java | 29 --- .../backend/user/dto/DetailInfoDTO.java | 34 --- .../backend/user/dto/SignUpRequestDTO.java | 33 +++ .../project/backend/user/dto/UserEnums.java | 53 +++++ .../backend/user/dto/UserInfoResponseDTO.java | 31 --- .../backend/user/dto/UserResponseDTO.java | 12 + .../backend/user/entity/BasicInfo.java | 49 ---- .../backend/user/entity/DetailInfo.java | 59 ----- .../project/backend/user/entity/User.java | 45 ++++ .../backend/user/entity/UserProfile.java | 51 ++++ .../user/repository/BasicInfoRepository.java | 13 -- .../user/repository/DetailInfoRepository.java | 17 -- .../resources/application-example.properties | 33 --- 16 files changed, 337 insertions(+), 439 deletions(-) create mode 100644 src/main/java/project/backend/user/UserRepository.java rename src/main/java/project/backend/user/{UserInfoService.java => UserService.java} (54%) delete mode 100644 src/main/java/project/backend/user/dto/BasicInfoDTO.java delete mode 100644 src/main/java/project/backend/user/dto/DetailInfoDTO.java create mode 100644 src/main/java/project/backend/user/dto/SignUpRequestDTO.java create mode 100644 src/main/java/project/backend/user/dto/UserEnums.java delete mode 100644 src/main/java/project/backend/user/dto/UserInfoResponseDTO.java create mode 100644 src/main/java/project/backend/user/dto/UserResponseDTO.java delete mode 100644 src/main/java/project/backend/user/entity/BasicInfo.java delete mode 100644 src/main/java/project/backend/user/entity/DetailInfo.java create mode 100644 src/main/java/project/backend/user/entity/User.java create mode 100644 src/main/java/project/backend/user/entity/UserProfile.java delete mode 100644 src/main/java/project/backend/user/repository/BasicInfoRepository.java delete mode 100644 src/main/java/project/backend/user/repository/DetailInfoRepository.java delete mode 100644 src/main/resources/application-example.properties diff --git a/src/main/java/project/backend/user/UserInfoController.java b/src/main/java/project/backend/user/UserInfoController.java index 987921e..d5cc7c2 100644 --- a/src/main/java/project/backend/user/UserInfoController.java +++ b/src/main/java/project/backend/user/UserInfoController.java @@ -1,91 +1,27 @@ package project.backend.user; -import java.util.List; - import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import project.backend.user.dto.BasicInfoDTO; -import project.backend.user.dto.DetailInfoDTO; -import project.backend.user.dto.UserInfoResponseDTO; +import project.backend.user.dto.SignUpRequestDTO; +import project.backend.user.dto.UserResponseDTO; @RestController -@RequestMapping("/api/user-info") +@RequestMapping("/users") @RequiredArgsConstructor -@Slf4j public class UserInfoController { - private final UserInfoService userInfoService; - + private final UserService userService; + // 기본 정보 저장 - @PostMapping("/basic") - public ResponseEntity saveBasicInfo(@Valid @RequestBody BasicInfoDTO basicInfoDTO) { - log.info("기본 정보 저장 요청: {}", basicInfoDTO.getName()); - BasicInfoDTO savedBasicInfo = userInfoService.saveBasicInfo(basicInfoDTO); - - return ResponseEntity.status(HttpStatus.CREATED).body(savedBasicInfo); - } - - // 상세 정보 저장(multipart/form-data) - @PostMapping(value = "/detail", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity saveDetailInfo( - @RequestParam("basicInfoId") Long basicInfoId, - @RequestParam(value = "profileImage", required = false) MultipartFile profileImage, - @RequestParam(value = "place", required = false) String place, - @RequestParam(value = "drinkingFrequency", required = false) String drinkingFrequency, - @RequestParam(value = "smokingStatus", required = false) String smokingStatus, - @RequestParam(value = "height", required = false) Integer height, - @RequestParam(value = "pet", required = false) String pet, - @RequestParam(value = "religion", required = false) String religion, - @RequestParam(value = "childPlan", required = false) String childPlan, - @RequestParam(value = "mbti", required = false) String mbti) { - - log.info("상세 정보 저장 요청 - 기본정보 ID: {}", basicInfoId); - - DetailInfoDTO detailInfoDTO = DetailInfoDTO.builder() - .basicInfoId(basicInfoId) - .profileImage(profileImage) - .place(place) - .drinkingFrequency(drinkingFrequency) - .smokingStatus(smokingStatus) - .height(height) - .pet(pet) - .religion(religion) - .childPlan(childPlan) - .mbti(mbti) - .build(); - - try { - DetailInfoDTO savedDetailInfo = userInfoService.saveDetailInfo(detailInfoDTO); - return ResponseEntity.status(HttpStatus.CREATED).body(savedDetailInfo); - } catch(Exception e) { - log.error("상세 정보 저장 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - // 테스트용 API - @GetMapping("/{userId}") - public ResponseEntity getUserInfo(@PathVariable("userId") Long userId) { - log.info("사용자 정보 조회 요청 - 사용자 ID: {}", userId); - UserInfoResponseDTO userInfo = userInfoService.getUserInfo(userId); - return ResponseEntity.ok(userInfo); - } - - @GetMapping("/same-place") - public ResponseEntity> getUsersBySamePlace(@RequestParam("basicInfoId") Long basicInfoId) { - List users = userInfoService.getUsersBySamePlace(basicInfoId); - return ResponseEntity.ok(users); + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody SignUpRequestDTO requestDTO) { + UserResponseDTO response = userService.registerNewUser(requestDTO); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); } } \ No newline at end of file diff --git a/src/main/java/project/backend/user/UserRepository.java b/src/main/java/project/backend/user/UserRepository.java new file mode 100644 index 0000000..8fa5448 --- /dev/null +++ b/src/main/java/project/backend/user/UserRepository.java @@ -0,0 +1,13 @@ +package project.backend.user; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import project.backend.user.entity.User; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/project/backend/user/UserInfoService.java b/src/main/java/project/backend/user/UserService.java similarity index 54% rename from src/main/java/project/backend/user/UserInfoService.java rename to src/main/java/project/backend/user/UserService.java index 6a89a09..0c5a72a 100644 --- a/src/main/java/project/backend/user/UserInfoService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -14,49 +14,69 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import project.backend.user.dto.BasicInfoDTO; -import project.backend.user.dto.DetailInfoDTO; -import project.backend.user.dto.UserInfoResponseDTO; -import project.backend.user.entity.BasicInfo; -import project.backend.user.entity.DetailInfo; -import project.backend.user.repository.BasicInfoRepository; -import project.backend.user.repository.DetailInfoRepository; +import project.backend.user.dto.SignUpRequestDTO; +import project.backend.user.dto.UserResponseDTO; +import project.backend.user.entity.User; +import project.backend.user.entity.UserProfile; @Service @RequiredArgsConstructor -@Slf4j -@Transactional -public class UserInfoService { +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository repository; - private final BasicInfoRepository basicInfoRepository; - private final DetailInfoRepository detailInfoRepository; - @Value("${file.upload-dir:uploads/profile}") private String uploadDir; - - // 기본 정보 저장 + + @Transactional + public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { + UserProfile userProfile = UserProfile.builder() + .job(requestDTO.getJob()) + .region(requestDTO.getRegion()) + .drinkingFrequency(requestDTO.getDrinkingFrequency()) + .smokingStatus(requestDTO.getSmokingStatus()) + .height(requestDTO.getHeight()) + .petPreference(requestDTO.getPetPreference()) + .religion(requestDTO.getReligion()) + .contactFrequency(requestDTO.getContactFrequency()) + .mbti(requestDTO.getMbti()) + .build(); + + User user = User.builder() + .name(requestDTO.getName()) + .gender(requestDTO.getGender()) + .birthDate(requestDTO.getBirthdate()) + .userProfile(userProfile) + .build(); + + repository.save(user); + + return UserResponseDTO.builder().id(user.getId()).name(user.getName()).build(); + } + + /*// 기본 정보 저장 public BasicInfoDTO saveBasicInfo(BasicInfoDTO basicInfoDTO) { - BasicInfo basicInfo = BasicInfo.builder() + User user = User.builder() .name(basicInfoDTO.getName()) .gender(basicInfoDTO.getGender()) .birthDate(basicInfoDTO.getBirthDate()) .build(); - BasicInfo savedBasicInfo = basicInfoRepository.save(basicInfo); + User savedUser = repository.save(user); return BasicInfoDTO.builder() - .id(savedBasicInfo.getId()) - .name(savedBasicInfo.getName()) - .gender(savedBasicInfo.getGender()) - .birthDate(savedBasicInfo.getBirthDate()) + .id(savedUser.getId()) + .name(savedUser.getName()) + .gender(savedUser.getGender()) + .birthDate(savedUser.getBirthDate()) .build(); } // 상세 정보 저장 public DetailInfoDTO saveDetailInfo(DetailInfoDTO detailInfoDTO) throws IOException { // 기본 정보 조회 - BasicInfo basicInfo = basicInfoRepository.findById(detailInfoDTO.getBasicInfoId()) + User user = repository.findById(detailInfoDTO.getBasicInfoId()) .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + detailInfoDTO.getBasicInfoId())); // 프로필 이미지 처리 @@ -65,8 +85,8 @@ public DetailInfoDTO saveDetailInfo(DetailInfoDTO detailInfoDTO) throws IOExcept profileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); } - DetailInfo detailInfo = DetailInfo.builder() - .basicInfo(basicInfo) + UserProfile userProfile = UserProfile.builder() + .user(user) .profileImagePath(profileImagePath) .place(detailInfoDTO.getPlace()) .drinkingFrequency(detailInfoDTO.getDrinkingFrequency()) @@ -78,49 +98,49 @@ public DetailInfoDTO saveDetailInfo(DetailInfoDTO detailInfoDTO) throws IOExcept .mbti(detailInfoDTO.getMbti()) .build(); - DetailInfo savedDetailInfo = detailInfoRepository.save(detailInfo); + UserProfile savedUserProfile = detailInfoRepository.save(userProfile); return DetailInfoDTO.builder() - .id(savedDetailInfo.getId()) - .basicInfoId(savedDetailInfo.getBasicInfo().getId()) - .profileImagePath(savedDetailInfo.getProfileImagePath()) - .place(savedDetailInfo.getPlace()) - .drinkingFrequency(savedDetailInfo.getDrinkingFrequency()) - .smokingStatus(savedDetailInfo.getSmokingStatus()) - .height(savedDetailInfo.getHeight()) - .pet(savedDetailInfo.getPet()) - .religion(savedDetailInfo.getReligion()) - .childPlan(savedDetailInfo.getChildPlan()) - .mbti(savedDetailInfo.getMbti()) + .id(savedUserProfile.getId()) + .basicInfoId(savedUserProfile.getUser().getId()) + .profileImagePath(savedUserProfile.getProfileImagePath()) + .place(savedUserProfile.getPlace()) + .drinkingFrequency(savedUserProfile.getDrinkingFrequency()) + .smokingStatus(savedUserProfile.getSmokingStatus()) + .height(savedUserProfile.getHeight()) + .pet(savedUserProfile.getPet()) + .religion(savedUserProfile.getReligion()) + .childPlan(savedUserProfile.getChildPlan()) + .mbti(savedUserProfile.getMbti()) .build(); } // 전체 사용자 정보 조회 @Transactional(readOnly = true) public UserInfoResponseDTO getUserInfo(Long basicInfoId) { - BasicInfo basicInfo = basicInfoRepository.findById(basicInfoId) + User user = repository.findById(basicInfoId) .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + basicInfoId)); - DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(basicInfoId) + UserProfile userProfile = detailInfoRepository.findByBasicInfoId(basicInfoId) .orElse(null); UserInfoResponseDTO.UserInfoResponseDTOBuilder builder = UserInfoResponseDTO.builder() - .basicInfoId(basicInfo.getId()) - .name(basicInfo.getName()) - .gender(basicInfo.getGender()) - .birthDate(basicInfo.getBirthDate()); + .basicInfoId(user.getId()) + .name(user.getName()) + .gender(user.getGender()) + .birthDate(user.getBirthDate()); - if (detailInfo != null) { - builder.detailInfoId(basicInfo.getId()) - .profileImagePath(detailInfo.getProfileImagePath()) - .place(detailInfo.getPlace()) - .drinkingFrequency(detailInfo.getDrinkingFrequency()) - .smokingStatus(detailInfo.getSmokingStatus()) - .height(detailInfo.getHeight()) - .pet(detailInfo.getPet()) - .religion(detailInfo.getReligion()) - .childPlan(detailInfo.getChildPlan()) - .mbti(detailInfo.getMbti()); + if (userProfile != null) { + builder.detailInfoId(user.getId()) + .profileImagePath(userProfile.getProfileImagePath()) + .place(userProfile.getPlace()) + .drinkingFrequency(userProfile.getDrinkingFrequency()) + .smokingStatus(userProfile.getSmokingStatus()) + .height(userProfile.getHeight()) + .pet(userProfile.getPet()) + .religion(userProfile.getReligion()) + .childPlan(userProfile.getChildPlan()) + .mbti(userProfile.getMbti()); } return builder.build(); @@ -128,59 +148,59 @@ public UserInfoResponseDTO getUserInfo(Long basicInfoId) { // 기본 정보 수정 public BasicInfoDTO updateBasicInfo(Long id, BasicInfoDTO basicInfoDTO) { - BasicInfo basicInfo = basicInfoRepository.findById(id) + User user = repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + id)); - basicInfo.setName(basicInfoDTO.getName()); - basicInfo.setGender(basicInfoDTO.getGender()); - basicInfo.setBirthDate(basicInfoDTO.getBirthDate()); + user.setName(basicInfoDTO.getName()); + user.setGender(basicInfoDTO.getGender()); + user.setBirthDate(basicInfoDTO.getBirthDate()); - BasicInfo updatedBasicInfo = basicInfoRepository.save(basicInfo); + User updatedUser = repository.save(user); return BasicInfoDTO.builder() - .id(updatedBasicInfo.getId()) - .name(updatedBasicInfo.getName()) - .gender(updatedBasicInfo.getGender()) - .birthDate(updatedBasicInfo.getBirthDate()) + .id(updatedUser.getId()) + .name(updatedUser.getName()) + .gender(updatedUser.getGender()) + .birthDate(updatedUser.getBirthDate()) .build(); } // 상세 정보 수정 public DetailInfoDTO updateDetailInfo(Long id, DetailInfoDTO detailInfoDTO) throws IOException { - DetailInfo detailInfo = detailInfoRepository.findById(id) + UserProfile userProfile = detailInfoRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + id)); // 새로운 프로필 이미지가 있으면 처리 if (detailInfoDTO.getProfileImage() != null && !detailInfoDTO.getProfileImage().isEmpty()) { // 기존 이미지 삭제(선택사항) - if (detailInfo.getProfileImagePath() != null) { - deleteProfileImage(detailInfo.getProfileImagePath()); + if (userProfile.getProfileImagePath() != null) { + deleteProfileImage(userProfile.getProfileImagePath()); } String newProfileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); - detailInfo.setProfileImagePath(newProfileImagePath); + userProfile.setProfileImagePath(newProfileImagePath); } - detailInfo.setPlace(detailInfoDTO.getPlace()); - detailInfo.setDrinkingFrequency(detailInfoDTO.getDrinkingFrequency()); - detailInfo.setSmokingStatus(detailInfoDTO.getSmokingStatus()); - detailInfo.setHeight(detailInfoDTO.getHeight()); - detailInfo.setPet(detailInfoDTO.getPet()); - detailInfo.setReligion(detailInfoDTO.getReligion()); - detailInfo.setChildPlan(detailInfoDTO.getChildPlan()); - detailInfo.setMbti(detailInfoDTO.getMbti()); + userProfile.setPlace(detailInfoDTO.getPlace()); + userProfile.setDrinkingFrequency(detailInfoDTO.getDrinkingFrequency()); + userProfile.setSmokingStatus(detailInfoDTO.getSmokingStatus()); + userProfile.setHeight(detailInfoDTO.getHeight()); + userProfile.setPet(detailInfoDTO.getPet()); + userProfile.setReligion(detailInfoDTO.getReligion()); + userProfile.setChildPlan(detailInfoDTO.getChildPlan()); + userProfile.setMbti(detailInfoDTO.getMbti()); - DetailInfo updatedDetailInfo = detailInfoRepository.save(detailInfo); + UserProfile updatedUserProfile = detailInfoRepository.save(userProfile); return DetailInfoDTO.builder() - .id(updatedDetailInfo.getId()) - .place(updatedDetailInfo.getPlace()) - .drinkingFrequency(updatedDetailInfo.getDrinkingFrequency()) - .smokingStatus(updatedDetailInfo.getSmokingStatus()) - .height(updatedDetailInfo.getHeight()) - .pet(updatedDetailInfo.getPet()) - .religion(updatedDetailInfo.getReligion()) - .childPlan(updatedDetailInfo.getChildPlan()) - .mbti(updatedDetailInfo.getMbti()) + .id(updatedUserProfile.getId()) + .place(updatedUserProfile.getPlace()) + .drinkingFrequency(updatedUserProfile.getDrinkingFrequency()) + .smokingStatus(updatedUserProfile.getSmokingStatus()) + .height(updatedUserProfile.getHeight()) + .pet(updatedUserProfile.getPet()) + .religion(updatedUserProfile.getReligion()) + .childPlan(updatedUserProfile.getChildPlan()) + .mbti(updatedUserProfile.getMbti()) .build(); } @@ -188,18 +208,18 @@ public DetailInfoDTO updateDetailInfo(Long id, DetailInfoDTO detailInfoDTO) thro @Transactional(readOnly = true) public List getUsersBySamePlace(Long myBasicInfoId) { // 내 상세정보 가져오기 - DetailInfo myDetail = detailInfoRepository.findByBasicInfoId(myBasicInfoId) + UserProfile myDetail = detailInfoRepository.findByBasicInfoId(myBasicInfoId) .orElseThrow(() -> new IllegalArgumentException("상세정보를 찾을 수 없습니다.")); String myPlace = myDetail.getPlace(); // 같은 지역 사용자 전체 조회 - List usersInSamePlace = detailInfoRepository.findByPlace(myPlace); + List usersInSamePlace = detailInfoRepository.findByPlace(myPlace); return usersInSamePlace.stream() - .filter(detail -> !detail.getBasicInfo().getId().equals(myBasicInfoId)) // 자신 제외 + .filter(detail -> !detail.getUser().getId().equals(myBasicInfoId)) // 자신 제외 .map(detail -> { - BasicInfo basic = detail.getBasicInfo(); + User basic = detail.getUser(); return UserInfoResponseDTO.builder() .basicInfoId(basic.getId()) @@ -223,17 +243,17 @@ public List getUsersBySamePlace(Long myBasicInfoId) { // 회원 탈퇴 (MyPageService에서 위임 호출) public void deleteUser(Long userId) { - BasicInfo basicInfo = basicInfoRepository.findById(userId) + User user = repository.findById(userId) .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); // 프로필 이미지 삭제 - DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId).orElse(null); - if (detailInfo != null && detailInfo.getProfileImagePath() != null) { - deleteProfileImage(detailInfo.getProfileImagePath()); + UserProfile userProfile = detailInfoRepository.findByBasicInfoId(userId).orElse(null); + if (userProfile != null && userProfile.getProfileImagePath() != null) { + deleteProfileImage(userProfile.getProfileImagePath()); } // DB에서 삭제 (Cascade 설정으로 DetailInfo도 함께 삭제됨) - basicInfoRepository.delete(basicInfo); + repository.delete(user); log.info("회원 탈퇴 완료 - 사용자 ID: {}", userId); } @@ -272,19 +292,19 @@ private void deleteProfileImage(String imagePath) { // MyPageService 위임 호출용 // 기존 프로필 이미지를 삭제하고 새로운 프로필 이미지 저장 후 경로 반환 public String updateProfileImage(Long userId, MultipartFile newImage) throws IOException { - DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) + UserProfile userProfile = detailInfoRepository.findByBasicInfoId(userId) .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + userId)); // 기존 이미지 삭제 - if (detailInfo.getProfileImagePath() != null) { - deleteProfileImage(detailInfo.getProfileImagePath()); + if (userProfile.getProfileImagePath() != null) { + deleteProfileImage(userProfile.getProfileImagePath()); } // 새 이미지 저장 String newPath = saveProfileImage(newImage); - detailInfo.setProfileImagePath(newPath); - detailInfoRepository.save(detailInfo); + userProfile.setProfileImagePath(newPath); + detailInfoRepository.save(userProfile); return newPath; - } + }*/ } \ No newline at end of file diff --git a/src/main/java/project/backend/user/dto/BasicInfoDTO.java b/src/main/java/project/backend/user/dto/BasicInfoDTO.java deleted file mode 100644 index 417b562..0000000 --- a/src/main/java/project/backend/user/dto/BasicInfoDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package project.backend.user.dto; - -import java.time.LocalDate; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class BasicInfoDTO { - private Long id; - - @NotBlank(message = "이름은 필수 입력 값입니다.") - private String name; - - @NotBlank(message = "성별은 필수 입력 값입니다.") - private String gender; - - @NotNull(message = "생년월일은 필수 입력 값입니다.") - private LocalDate birthDate; -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/dto/DetailInfoDTO.java b/src/main/java/project/backend/user/dto/DetailInfoDTO.java deleted file mode 100644 index 8681fda..0000000 --- a/src/main/java/project/backend/user/dto/DetailInfoDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package project.backend.user.dto; - -import org.springframework.web.multipart.MultipartFile; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class DetailInfoDTO { - private Long id; - private Long basicInfoId; - - @JsonIgnore - private MultipartFile profileImage; - - private String profileImagePath; - private String place; - private String drinkingFrequency; - private String smokingStatus; - private Integer height; - private String pet; - private String religion; - private String childPlan; - private String mbti; -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java new file mode 100644 index 0000000..04fc251 --- /dev/null +++ b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java @@ -0,0 +1,33 @@ +package project.backend.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +public class SignUpRequestDTO { + + // 기본정보 + private String name; + private LocalDate birthdate; + private UserEnums.Gender gender; + + // 상세정보 + private UserEnums.Job job; + private String region; + private UserEnums.DrinkingFrequency drinkingFrequency; + private UserEnums.SmokingStatus smokingStatus; + private Integer height; + private UserEnums.PetPreference petPreference; + private UserEnums.Religion religion; + private UserEnums.ContactFrequency contactFrequency; + private UserEnums.Mbti mbti; + + //자기소개서 + private String introduction; + + //이미지 url...? + //private String profileImage; +} diff --git a/src/main/java/project/backend/user/dto/UserEnums.java b/src/main/java/project/backend/user/dto/UserEnums.java new file mode 100644 index 0000000..a3c9cb8 --- /dev/null +++ b/src/main/java/project/backend/user/dto/UserEnums.java @@ -0,0 +1,53 @@ +package project.backend.user.dto; + +public class UserEnums { + + public enum Gender { + MALE, + FEMALE + } + + public enum Job { + UNEMPLOYED, + STUDENT, + EMPLOYEE + } + + public enum DrinkingFrequency { + NONE, + ONCE_OR_TWICE_PER_WEEK, + THREE_TIMES_OR_MORE_PER_WEEK + } + + public enum SmokingStatus { + NON_SMOKER, + SMOKER + } + + public enum PetPreference { + NONE, + DOG, + CAT, + OTHER + } + + public enum Religion { + NONE, // 무교 + CATHOLIC, // 천주교 + CHRISTIAN, // 기독교 + BUDDHIST, // 불교 + OTHER // 기타 + } + + public enum ContactFrequency { + IMPORTANT, + NOT_IMPORTANT + } + + public enum Mbti { + INTJ, INTP, ENTJ, ENTP, + INFJ, INFP, ENFJ, ENFP, + ISTJ, ISFJ, ESTJ, ESFJ, + ISTP, ISFP, ESTP, ESFP + } +} diff --git a/src/main/java/project/backend/user/dto/UserInfoResponseDTO.java b/src/main/java/project/backend/user/dto/UserInfoResponseDTO.java deleted file mode 100644 index 019b746..0000000 --- a/src/main/java/project/backend/user/dto/UserInfoResponseDTO.java +++ /dev/null @@ -1,31 +0,0 @@ -package project.backend.user.dto; - -import java.time.LocalDate; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class UserInfoResponseDTO { - private Long basicInfoId; - private String name; - private String gender; - private LocalDate birthDate; - private Long detailInfoId; - private String profileImagePath; - private String place; - private String drinkingFrequency; - private String smokingStatus; - private Integer height; - private String pet; - private String religion; - private String childPlan; - private String mbti; -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/dto/UserResponseDTO.java b/src/main/java/project/backend/user/dto/UserResponseDTO.java new file mode 100644 index 0000000..68435c7 --- /dev/null +++ b/src/main/java/project/backend/user/dto/UserResponseDTO.java @@ -0,0 +1,12 @@ +package project.backend.user.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserResponseDTO { + + private Long id; + private String name; +} diff --git a/src/main/java/project/backend/user/entity/BasicInfo.java b/src/main/java/project/backend/user/entity/BasicInfo.java deleted file mode 100644 index baff165..0000000 --- a/src/main/java/project/backend/user/entity/BasicInfo.java +++ /dev/null @@ -1,49 +0,0 @@ -package project.backend.user.entity; - -import java.time.LocalDate; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import project.backend.kakaoLogin.User; - -@Entity -@Table(name = "basic_info") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class BasicInfo { - @Id // 기본키 필드 - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) // 기본 정보 - 이름 - private String name; - - @Column(nullable = false) // 기본 정보 - 성별 - private String gender; // 남자 또는 여자 - - @Column(name = "birth_date", nullable = false) - private LocalDate birthDate; - - @OneToOne - @JoinColumn(name = "user_id") - private User user; - - @OneToOne(mappedBy = "basicInfo", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private DetailInfo detailInfo; -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/entity/DetailInfo.java b/src/main/java/project/backend/user/entity/DetailInfo.java deleted file mode 100644 index 2542fa5..0000000 --- a/src/main/java/project/backend/user/entity/DetailInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -package project.backend.user.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Entity -@Table(name = "detail_info") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class DetailInfo { - @Id // 기본키 필드 - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne - @JoinColumn(name = "basic_info_id", nullable = false) - private BasicInfo basicInfo; - - @Column(name = "profile_image_path") // 상세 정보 - 프로필 사진 - private String profileImagePath; - - @Column(name = "place") // 상세 정보 - 거주 지역 - private String place; // 예: 서울특별시, 경기도 용인시 - - @Column(name = "drinking_frequency") // 상세 정보 - 음주빈도 - private String drinkingFrequency; // 안 마심, 가끔 마심, 적당히 마심, 자주 마심 - - @Column(name = "smoking_status") // 상세 정보 - 흡연여부 - private String smokingStatus; // 비흡연, 가끔 흡연, 흡연 - - @Column(name = "height") // 상세 정보 - 키 - private Integer height; - - @Column(name = "pet") // 상세 정보 - 반려동물 - private String pet; // 없음, 강아지, 고양이, 기타 - - @Column(name = "religion") // 상세 정보 - 종교 - private String religion; // 무교, 불교, 기독교, 천주교, 기타 - - @Column(name = "child_plan") // 상세 정보 - 자녀계획 - private String childPlan; // 원함, 상관없음, 원하지 않음 - - @Column(name = "mbti", length = 4) // 상세 정보 - MBTI - private String mbti; -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/entity/User.java b/src/main/java/project/backend/user/entity/User.java new file mode 100644 index 0000000..558efe8 --- /dev/null +++ b/src/main/java/project/backend/user/entity/User.java @@ -0,0 +1,45 @@ +package project.backend.user.entity; + +import java.time.LocalDate; + +import jakarta.persistence.*; +import lombok.*; +import project.backend.user.dto.UserEnums; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private UserEnums.Gender gender; + + private LocalDate birthDate; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) + private UserProfile userProfile; + + @Builder + public User(String name, UserEnums.Gender gender, LocalDate birthDate, UserProfile userProfile) { + this.name = name; + this.gender = gender; + this.birthDate = birthDate; + + this.setUserProfile(userProfile); + } + + private void setUserProfile(UserProfile userProfile) { + this.userProfile = userProfile; + + if (userProfile != null) { + userProfile.setUser(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java new file mode 100644 index 0000000..35f79e4 --- /dev/null +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -0,0 +1,51 @@ +package project.backend.user.entity; + +import jakarta.persistence.*; +import lombok.*; +import project.backend.user.dto.UserEnums; + +@Entity +@Table(name = "user_profiles") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Setter + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + private String profileImagePath; + + @Enumerated(EnumType.STRING) + private UserEnums.Job job; + + private String region; + + @Enumerated(EnumType.STRING) + private UserEnums.DrinkingFrequency drinkingFrequency; + + @Enumerated(EnumType.STRING) + private UserEnums.SmokingStatus smokingStatus; + + private Integer height; + + @Enumerated(EnumType.STRING) + private UserEnums.PetPreference petPreference; + + @Enumerated(EnumType.STRING) + private UserEnums.Religion religion; + + @Enumerated(EnumType.STRING) + private UserEnums.ContactFrequency contactFrequency; + + @Enumerated(EnumType.STRING) + private UserEnums.Mbti mbti; + +} \ No newline at end of file diff --git a/src/main/java/project/backend/user/repository/BasicInfoRepository.java b/src/main/java/project/backend/user/repository/BasicInfoRepository.java deleted file mode 100644 index 6c90cbc..0000000 --- a/src/main/java/project/backend/user/repository/BasicInfoRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package project.backend.user.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import project.backend.user.entity.BasicInfo; - -@Repository -public interface BasicInfoRepository extends JpaRepository { - Optional findByName(String name); -} \ No newline at end of file diff --git a/src/main/java/project/backend/user/repository/DetailInfoRepository.java b/src/main/java/project/backend/user/repository/DetailInfoRepository.java deleted file mode 100644 index 63afa6a..0000000 --- a/src/main/java/project/backend/user/repository/DetailInfoRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package project.backend.user.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import project.backend.user.entity.DetailInfo; - -@Repository -public interface DetailInfoRepository extends JpaRepository { - Optional findByBasicInfoId(Long basicInfoId); - - // 같은 거주 지역(place)인 사용자들 목록 - List findByPlace(String place); -} \ No newline at end of file diff --git a/src/main/resources/application-example.properties b/src/main/resources/application-example.properties deleted file mode 100644 index 5ec3e74..0000000 --- a/src/main/resources/application-example.properties +++ /dev/null @@ -1,33 +0,0 @@ -spring.application.name=backend - -# MySQL -spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false -spring.datasource.username=your_username -spring.datasource.password=your_password -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - -# JPA -spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect - -# File Upload Settings -spring.servlet.multipart.enabled=true -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB -file.upload-dir=uploads/profile - -# JWT Settings -jwt.issuer=your_issuer -jwt.secret-key=your_secret_key - -# Kakao OAuth Settings -spring.security.oauth2.client.registration.kakao.client-id=your_client_id -spring.security.oauth2.client.registration.kakao.client-name=your_client_name -spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/auth/kakao/callback -spring.security.oauth2.client.registration.kakao.scope=account_email -spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize -spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token -spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me -spring.security.oauth2.client.provider.kakao.user-name-attribute=id \ No newline at end of file From 7f85470d296100754b56a28050df9008ad6b5459 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Mon, 3 Nov 2025 00:38:21 +0900 Subject: [PATCH 14/32] =?UTF-8?q?refactor=20:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사진 로직, 나이계산 로직 아직 수정 x --- .../backend/mypage/MyPageController.java | 92 ++------- .../backend/mypage/MyPageDisplayDTO.java | 27 --- .../project/backend/mypage/MyPageService.java | 131 ++----------- .../backend/mypage/ProfileEditDTO.java | 36 ---- .../backend/mypage/ProfileUpdateDTO.java | 49 ----- .../backend/mypage/dto/MyPageDisplayDTO.java | 55 ++++++ .../backend/user/UserInfoController.java | 1 + .../project/backend/user/UserRepository.java | 6 +- .../project/backend/user/UserService.java | 183 +++--------------- .../backend/user/dto/SignUpRequestDTO.java | 4 +- .../backend/user/dto/UserProfileDTO.java | 17 ++ .../backend/user/dto/UserResponseDTO.java | 4 +- .../backend/user/entity/UserProfile.java | 52 ++++- 13 files changed, 196 insertions(+), 461 deletions(-) delete mode 100644 src/main/java/project/backend/mypage/MyPageDisplayDTO.java delete mode 100644 src/main/java/project/backend/mypage/ProfileEditDTO.java delete mode 100644 src/main/java/project/backend/mypage/ProfileUpdateDTO.java create mode 100644 src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java create mode 100644 src/main/java/project/backend/user/dto/UserProfileDTO.java diff --git a/src/main/java/project/backend/mypage/MyPageController.java b/src/main/java/project/backend/mypage/MyPageController.java index 9cf24a3..97077f3 100644 --- a/src/main/java/project/backend/mypage/MyPageController.java +++ b/src/main/java/project/backend/mypage/MyPageController.java @@ -1,99 +1,43 @@ package project.backend.mypage; -import java.io.IOException; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import project.backend.mypage.dto.MyPageDisplayDTO; +import project.backend.user.dto.UserProfileDTO; @RestController -@RequestMapping("/api/my-page") +@RequestMapping("/my-page") @RequiredArgsConstructor -@Slf4j public class MyPageController { + private final MyPageService myPageService; // 마이페이지 정보 조회 @GetMapping("/{userId}") public ResponseEntity getMyPageInfo(@PathVariable("userId") Long userId) { - log.info("마이페이지 조회 요청 - 사용자 ID: {}", userId); MyPageDisplayDTO myPageInfo = myPageService.getMyPageInfo(userId); return ResponseEntity.ok(myPageInfo); } - - // 프로필 수정 화면용 전체 정보 조회 - @GetMapping("/profile/{userId}") - public ResponseEntity getProfileForEdit(@PathVariable("userId") Long userId) { - log.info("프로필 수정 화면 정보 요청 - 사용자 ID: {}", userId); - ProfileEditDTO profileEdit = myPageService.getProfileForEdit(userId); - - return ResponseEntity.ok(profileEdit); - } - - // 프로필 전체 수정 (기본 정보 + 상세 정보, 이미지 포함) - @PutMapping(value = "/profile/{userId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateProfile( - @PathVariable(value = "userId") Long userId, - @RequestParam(value = "name", required = false) String name, - @RequestParam(value = "gender", required = false) String gender, - @RequestParam(value = "birthDate", required = false) String birthDate, - @RequestParam(value = "profileImage", required = false) MultipartFile profileImage, - @RequestParam(value = "place", required = false) String place, - @RequestParam(value = "drinkingFrequency", required = false) String drinkingFrequency, - @RequestParam(value = "smokingStatus", required = false) String smokingStatus, - @RequestParam(value = "height", required = false) Integer height, - @RequestParam(value = "pet", required = false) String pet, - @RequestParam(value = "religion", required = false) String religion, - @RequestParam(value = "childPlan", required = false) String childPlan, - @RequestParam(value = "mbti", required = false) String mbti - ) throws IOException { - - log.info("프로필 수정 요청 - 사용자 ID: {}", userId); - - ProfileUpdateDTO updateDTO = ProfileUpdateDTO.builder() - .name(name) - .gender(gender) - .birthDate((birthDate != null && !birthDate.isEmpty()) ? LocalDate.parse(birthDate) : null) - .profileImage(profileImage) - .place(place) - .drinkingFrequency(drinkingFrequency) - .smokingStatus(smokingStatus) - .height(height) - .pet(pet) - .religion(religion) - .childPlan(childPlan) - .mbti(mbti) - .build(); - - ProfileEditDTO updatedProfile = myPageService.updateProfile(userId, updateDTO); - - return ResponseEntity.ok(updatedProfile); + + // 마이페이지 프로필 정보 수정 + @PatchMapping("/{userId}/profile") + public ResponseEntity updateProfile( + @PathVariable("userId") Long userId, + @RequestBody UserProfileDTO userProfileDTO) { + myPageService.editProfile(userId, userProfileDTO); + + return ResponseEntity.ok().build(); } + // 회원 탈퇴 @DeleteMapping("/{userId}") - public ResponseEntity> deleteUser(@PathVariable("userId") Long userId) { - log.info("회원 탈퇴 요청 - 사용자 ID: {}", userId); + public ResponseEntity deleteUser(@PathVariable("userId") Long userId) { myPageService.deleteUser(userId); - - Map response = new HashMap<>(); - response.put("message", "회원 탈퇴가 완료되었습니다."); // 회원 탈퇴 성공 여부 확인하려고 작성함 - response.put("userId", userId.toString()); - - return ResponseEntity.ok(response); + + return ResponseEntity.ok(userId); } } \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/MyPageDisplayDTO.java deleted file mode 100644 index 0c3ef47..0000000 --- a/src/main/java/project/backend/mypage/MyPageDisplayDTO.java +++ /dev/null @@ -1,27 +0,0 @@ -package project.backend.mypage; - -import java.time.LocalDate; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class MyPageDisplayDTO { - private Long userId; //basicInfoId - private String profileImagePath; // 프로필 사진 - private String name; // 이름 - private Integer age; // 만 나이 (계산값) - private String gender; // 성별 - // 여기에 직업 추가 - private String place; // 거주 지역 - private LocalDate birthDate; // 생년월일 - private String mbti; // MBTI - // 여기에 자기소개 추가 -} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/MyPageService.java b/src/main/java/project/backend/mypage/MyPageService.java index 220f4d2..730a528 100644 --- a/src/main/java/project/backend/mypage/MyPageService.java +++ b/src/main/java/project/backend/mypage/MyPageService.java @@ -1,142 +1,41 @@ package project.backend.mypage; -import java.io.IOException; import java.time.LocalDate; import java.time.Period; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import project.backend.user.dto.UserInfoResponseDTO; -import project.backend.user.entity.BasicInfo; -import project.backend.user.entity.DetailInfo; -import project.backend.user.repository.BasicInfoRepository; -import project.backend.user.repository.DetailInfoRepository; -import project.backend.user.UserInfoService; +import project.backend.mypage.dto.MyPageDisplayDTO; +import project.backend.user.UserService; +import project.backend.user.dto.UserProfileDTO; @Service @RequiredArgsConstructor -@Slf4j +@Transactional(readOnly = true) public class MyPageService { - private final UserInfoService userInfoService; - private final BasicInfoRepository basicInfoRepository; - private final DetailInfoRepository detailInfoRepository; + + private final UserService userService; // 마이페이지 조회 public MyPageDisplayDTO getMyPageInfo(Long userId) { - UserInfoResponseDTO userInfo = userInfoService.getUserInfo(userId); - - Integer age = calculateAge(userInfo.getBirthDate()); - - return MyPageDisplayDTO.builder() - .userId(userInfo.getBasicInfoId()) - .profileImagePath(userInfo.getProfileImagePath()) - .name(userInfo.getName()) - .age(age) - .gender(userInfo.getGender()) - .place(userInfo.getPlace()) - .birthDate(userInfo.getBirthDate()) - .mbti(userInfo.getMbti()) - .build(); + return userService.getUserInfo(userId); } - - // 프로필 수정 화면용 전체 정보 조회 - @Transactional(readOnly = true) - public ProfileEditDTO getProfileForEdit(Long userId) { - BasicInfo basicInfo = basicInfoRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); - - DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) - .orElse(null); - - ProfileEditDTO.ProfileEditDTOBuilder builder = ProfileEditDTO.builder() - .basicInfoId(basicInfo.getId()) - .name(basicInfo.getName()) - .gender(basicInfo.getGender()) - .birthDate(basicInfo.getBirthDate()); - - if (detailInfo != null) { - builder.detailInfoId(detailInfo.getId()) - .profileImagePath(detailInfo.getProfileImagePath()) - .place(detailInfo.getPlace()) - .drinkingFrequency(detailInfo.getDrinkingFrequency()) - .smokingStatus(detailInfo.getSmokingStatus()) - .height(detailInfo.getHeight()) - .pet(detailInfo.getPet()) - .religion(detailInfo.getReligion()) - .childPlan(detailInfo.getChildPlan()) - .mbti(detailInfo.getMbti()); - } - - return builder.build(); - } - - // 프로필 전체 수정 (기본 정보 + 상세 정보) - @Transactional - public ProfileEditDTO updateProfile(Long userId, ProfileUpdateDTO updateDTO) throws IOException { - BasicInfo basicInfo = basicInfoRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); - - // 기본 정보 업데이트 (null 체크) - if (updateDTO.getName() != null) basicInfo.setName(updateDTO.getName()); - if (updateDTO.getGender() != null) basicInfo.setGender(updateDTO.getGender()); - if (updateDTO.getBirthDate() != null) basicInfo.setBirthDate(updateDTO.getBirthDate()); - BasicInfo savedBasicInfo = basicInfoRepository.save(basicInfo); - - // 상세 정보 - DetailInfo detailInfo = detailInfoRepository.findByBasicInfoId(userId) - .orElse(DetailInfo.builder() - .basicInfo(savedBasicInfo) - .build()); - - // 프로필 이미지 처리 - if (updateDTO.getProfileImage() != null && !updateDTO.getProfileImage().isEmpty()) { - String newProfileImagePath = userInfoService.updateProfileImage(userId, updateDTO.getProfileImage()); - detailInfo.setProfileImagePath(newProfileImagePath); - } - - // null 체크 후 업데이트 - if (updateDTO.getPlace() != null) detailInfo.setPlace(updateDTO.getPlace()); - if (updateDTO.getDrinkingFrequency() != null) detailInfo.setDrinkingFrequency(updateDTO.getDrinkingFrequency()); - if (updateDTO.getSmokingStatus() != null) detailInfo.setSmokingStatus(updateDTO.getSmokingStatus()); - if (updateDTO.getHeight() != null) detailInfo.setHeight(updateDTO.getHeight()); - if (updateDTO.getPet() != null) detailInfo.setPet(updateDTO.getPet()); - if (updateDTO.getReligion() != null) detailInfo.setReligion(updateDTO.getReligion()); - if (updateDTO.getChildPlan() != null) detailInfo.setChildPlan(updateDTO.getChildPlan()); - if (updateDTO.getMbti() != null) detailInfo.setMbti(updateDTO.getMbti()); - - DetailInfo savedDetailInfo = detailInfoRepository.save(detailInfo); - - // 응답 DTO 생성 (수정 완료 후 전체 사용자 정보 반환) - return ProfileEditDTO.builder() - .basicInfoId(savedBasicInfo.getId()) - .name(savedBasicInfo.getName()) - .gender(savedBasicInfo.getGender()) - .birthDate(savedBasicInfo.getBirthDate()) - .detailInfoId(savedDetailInfo.getId()) - .profileImagePath(savedDetailInfo.getProfileImagePath()) - .place(savedDetailInfo.getPlace()) - .drinkingFrequency(savedDetailInfo.getDrinkingFrequency()) - .smokingStatus(savedDetailInfo.getSmokingStatus()) - .height(savedDetailInfo.getHeight()) - .pet(savedDetailInfo.getPet()) - .religion(savedDetailInfo.getReligion()) - .childPlan(savedDetailInfo.getChildPlan()) - .mbti(savedDetailInfo.getMbti()) - .build(); + // user 정보 수정 + @Transactional + public void editProfile(Long userId, UserProfileDTO userProfileDTO) { + userService.updateUserProfileInfo(userId, userProfileDTO); } - + // 회원 탈퇴 + @Transactional public void deleteUser(Long userId) { - userInfoService.deleteUser(userId); - log.info("회원 탈퇴 완료 - 사용자 ID: {}", userId); + userService.deleteUser(userId); } - // 나이 계산 (만 나이) + // 나이 계산 (만 나이) 아직 사용 x private Integer calculateAge(LocalDate birthDate) { return (birthDate == null) ? null : Period.between(birthDate, LocalDate.now()).getYears(); } diff --git a/src/main/java/project/backend/mypage/ProfileEditDTO.java b/src/main/java/project/backend/mypage/ProfileEditDTO.java deleted file mode 100644 index e771dfc..0000000 --- a/src/main/java/project/backend/mypage/ProfileEditDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -// 프로필 수정 화면용 - 전체 정보 - -package project.backend.mypage; - -import java.time.LocalDate; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ProfileEditDTO { - // 기본 정보 - private Long basicInfoId; - private String name; - private String gender; - private LocalDate birthDate; - - // 상세 정보 - private Long detailInfoId; - private String profileImagePath; - private String place; - private String drinkingFrequency; - private String smokingStatus; - private Integer height; - private String pet; - private String religion; - private String childPlan; - private String mbti; -} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/ProfileUpdateDTO.java b/src/main/java/project/backend/mypage/ProfileUpdateDTO.java deleted file mode 100644 index 713bd8e..0000000 --- a/src/main/java/project/backend/mypage/ProfileUpdateDTO.java +++ /dev/null @@ -1,49 +0,0 @@ -// 프로필 수정 요청용 - -package project.backend.mypage; - -import java.time.LocalDate; - -import org.springframework.web.multipart.MultipartFile; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ProfileUpdateDTO { - // 기본 정보 - @NotBlank(message = "이름은 필수 입력 값입니다.") - private String name; - - @NotBlank(message = "성별은 필수 입력 값입니다.") - private String gender; - - @NotBlank(message = "생년월일은 필수 입력 값입니다.") - private LocalDate birthDate; - - // 상세 정보 - private Long detailInfoId; - - @JsonIgnore - private MultipartFile profileImage; - - private String place; - private String drinkingFrequency; - private String smokingStatus; - private Integer height; - private String pet; - private String religion; - private String childPlan; - private String mbti; - -} \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java new file mode 100644 index 0000000..d219c23 --- /dev/null +++ b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java @@ -0,0 +1,55 @@ +package project.backend.mypage.dto; + +import lombok.Builder; +import lombok.Getter; +import project.backend.user.dto.UserEnums; +import project.backend.user.entity.User; +import project.backend.user.entity.UserProfile; + +import java.time.LocalDate; + +@Getter +public class MyPageDisplayDTO { + + // User 정보 + private Long userId; + private String name; + private UserEnums.Gender gender; + private LocalDate birthDate; + + // UserProfile 정보 + private UserEnums.Job job; + private String region; + private UserEnums.DrinkingFrequency drinkingFrequency; + private UserEnums.SmokingStatus smokingStatus; + private Integer height; + private UserEnums.PetPreference petPreference; + private UserEnums.Religion religion; + private UserEnums.ContactFrequency contactFrequency; + private UserEnums.Mbti mbti; + + @Builder + public MyPageDisplayDTO(User user, UserProfile profile) { + this.userId = user.getId(); + this.name = user.getName(); + this.gender = user.getGender(); + this.birthDate = user.getBirthDate(); + + if (profile != null) { + this.job = profile.getJob(); + this.region = profile.getRegion(); + this.drinkingFrequency = profile.getDrinkingFrequency(); + this.smokingStatus = profile.getSmokingStatus(); + this.height = profile.getHeight(); + this.petPreference = profile.getPetPreference(); + this.religion = profile.getReligion(); + this.contactFrequency = profile.getContactFrequency(); + this.mbti = profile.getMbti(); + } + } + + public static MyPageDisplayDTO fromEntity(User user) { + return new MyPageDisplayDTO(user, user.getUserProfile()); + } + +} diff --git a/src/main/java/project/backend/user/UserInfoController.java b/src/main/java/project/backend/user/UserInfoController.java index d5cc7c2..12604dd 100644 --- a/src/main/java/project/backend/user/UserInfoController.java +++ b/src/main/java/project/backend/user/UserInfoController.java @@ -15,6 +15,7 @@ @RequestMapping("/users") @RequiredArgsConstructor public class UserInfoController { + private final UserService userService; // 기본 정보 저장 diff --git a/src/main/java/project/backend/user/UserRepository.java b/src/main/java/project/backend/user/UserRepository.java index 8fa5448..40c868d 100644 --- a/src/main/java/project/backend/user/UserRepository.java +++ b/src/main/java/project/backend/user/UserRepository.java @@ -3,11 +3,15 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import project.backend.user.entity.User; @Repository public interface UserRepository extends JpaRepository { - Optional findByName(String name); + + @Query("SELECT u FROM User u JOIN FETCH u.userProfile WHERE u.id = :id") + Optional findByIdWithProfile(@Param("id") Long id); } \ No newline at end of file diff --git a/src/main/java/project/backend/user/UserService.java b/src/main/java/project/backend/user/UserService.java index 0c5a72a..017a37c 100644 --- a/src/main/java/project/backend/user/UserService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -1,20 +1,17 @@ package project.backend.user; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.UUID; - import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import project.backend.mypage.dto.MyPageDisplayDTO; import project.backend.user.dto.SignUpRequestDTO; +import project.backend.user.dto.UserEnums; +import project.backend.user.dto.UserProfileDTO; import project.backend.user.dto.UserResponseDTO; import project.backend.user.entity.User; import project.backend.user.entity.UserProfile; @@ -29,6 +26,7 @@ public class UserService { @Value("${file.upload-dir:uploads/profile}") private String uploadDir; + //회원가입 @Transactional public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { UserProfile userProfile = UserProfile.builder() @@ -52,158 +50,39 @@ public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { repository.save(user); - return UserResponseDTO.builder().id(user.getId()).name(user.getName()).build(); + return new UserResponseDTO(user.getId(), user.getName()); //과연 필요할까..??일단 넣음 } - /*// 기본 정보 저장 - public BasicInfoDTO saveBasicInfo(BasicInfoDTO basicInfoDTO) { - User user = User.builder() - .name(basicInfoDTO.getName()) - .gender(basicInfoDTO.getGender()) - .birthDate(basicInfoDTO.getBirthDate()) - .build(); - - User savedUser = repository.save(user); - - return BasicInfoDTO.builder() - .id(savedUser.getId()) - .name(savedUser.getName()) - .gender(savedUser.getGender()) - .birthDate(savedUser.getBirthDate()) - .build(); - } - - // 상세 정보 저장 - public DetailInfoDTO saveDetailInfo(DetailInfoDTO detailInfoDTO) throws IOException { - // 기본 정보 조회 - User user = repository.findById(detailInfoDTO.getBasicInfoId()) - .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + detailInfoDTO.getBasicInfoId())); - - // 프로필 이미지 처리 - String profileImagePath = null; - if (detailInfoDTO.getProfileImage() != null && !detailInfoDTO.getProfileImage().isEmpty()) { - profileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); - } - - UserProfile userProfile = UserProfile.builder() - .user(user) - .profileImagePath(profileImagePath) - .place(detailInfoDTO.getPlace()) - .drinkingFrequency(detailInfoDTO.getDrinkingFrequency()) - .smokingStatus(detailInfoDTO.getSmokingStatus()) - .height(detailInfoDTO.getHeight()) - .pet(detailInfoDTO.getPet()) - .religion(detailInfoDTO.getReligion()) - .childPlan(detailInfoDTO.getChildPlan()) - .mbti(detailInfoDTO.getMbti()) - .build(); - - UserProfile savedUserProfile = detailInfoRepository.save(userProfile); - - return DetailInfoDTO.builder() - .id(savedUserProfile.getId()) - .basicInfoId(savedUserProfile.getUser().getId()) - .profileImagePath(savedUserProfile.getProfileImagePath()) - .place(savedUserProfile.getPlace()) - .drinkingFrequency(savedUserProfile.getDrinkingFrequency()) - .smokingStatus(savedUserProfile.getSmokingStatus()) - .height(savedUserProfile.getHeight()) - .pet(savedUserProfile.getPet()) - .religion(savedUserProfile.getReligion()) - .childPlan(savedUserProfile.getChildPlan()) - .mbti(savedUserProfile.getMbti()) - .build(); - } // 전체 사용자 정보 조회 - @Transactional(readOnly = true) - public UserInfoResponseDTO getUserInfo(Long basicInfoId) { - User user = repository.findById(basicInfoId) - .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + basicInfoId)); - - UserProfile userProfile = detailInfoRepository.findByBasicInfoId(basicInfoId) - .orElse(null); - - UserInfoResponseDTO.UserInfoResponseDTOBuilder builder = UserInfoResponseDTO.builder() - .basicInfoId(user.getId()) - .name(user.getName()) - .gender(user.getGender()) - .birthDate(user.getBirthDate()); - - if (userProfile != null) { - builder.detailInfoId(user.getId()) - .profileImagePath(userProfile.getProfileImagePath()) - .place(userProfile.getPlace()) - .drinkingFrequency(userProfile.getDrinkingFrequency()) - .smokingStatus(userProfile.getSmokingStatus()) - .height(userProfile.getHeight()) - .pet(userProfile.getPet()) - .religion(userProfile.getReligion()) - .childPlan(userProfile.getChildPlan()) - .mbti(userProfile.getMbti()); - } - - return builder.build(); + public MyPageDisplayDTO getUserInfo(Long userId) { + User user = repository.findByIdWithProfile(userId) + .orElseThrow(() -> new EntityNotFoundException("User with id " + userId + " not found")); + + return MyPageDisplayDTO.fromEntity(user); } - - // 기본 정보 수정 - public BasicInfoDTO updateBasicInfo(Long id, BasicInfoDTO basicInfoDTO) { - User user = repository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("기본 정보를 찾을 수 없습니다. ID: " + id)); - - user.setName(basicInfoDTO.getName()); - user.setGender(basicInfoDTO.getGender()); - user.setBirthDate(basicInfoDTO.getBirthDate()); - - User updatedUser = repository.save(user); - - return BasicInfoDTO.builder() - .id(updatedUser.getId()) - .name(updatedUser.getName()) - .gender(updatedUser.getGender()) - .birthDate(updatedUser.getBirthDate()) - .build(); + + //상세 정보 수정 + @Transactional + public void updateUserProfileInfo(Long userId, UserProfileDTO userProfileDTO) { + User user = repository.findByIdWithProfile(userId) + .orElseThrow(() -> new EntityNotFoundException("User with id " + userId + " not found")); + + UserProfile userProfile = user.getUserProfile(); + userProfile.updateUserProfile(userProfileDTO); + } - - // 상세 정보 수정 - public DetailInfoDTO updateDetailInfo(Long id, DetailInfoDTO detailInfoDTO) throws IOException { - UserProfile userProfile = detailInfoRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + id)); - - // 새로운 프로필 이미지가 있으면 처리 - if (detailInfoDTO.getProfileImage() != null && !detailInfoDTO.getProfileImage().isEmpty()) { - // 기존 이미지 삭제(선택사항) - if (userProfile.getProfileImagePath() != null) { - deleteProfileImage(userProfile.getProfileImagePath()); - } - String newProfileImagePath = saveProfileImage(detailInfoDTO.getProfileImage()); - userProfile.setProfileImagePath(newProfileImagePath); - } - - userProfile.setPlace(detailInfoDTO.getPlace()); - userProfile.setDrinkingFrequency(detailInfoDTO.getDrinkingFrequency()); - userProfile.setSmokingStatus(detailInfoDTO.getSmokingStatus()); - userProfile.setHeight(detailInfoDTO.getHeight()); - userProfile.setPet(detailInfoDTO.getPet()); - userProfile.setReligion(detailInfoDTO.getReligion()); - userProfile.setChildPlan(detailInfoDTO.getChildPlan()); - userProfile.setMbti(detailInfoDTO.getMbti()); - - UserProfile updatedUserProfile = detailInfoRepository.save(userProfile); - - return DetailInfoDTO.builder() - .id(updatedUserProfile.getId()) - .place(updatedUserProfile.getPlace()) - .drinkingFrequency(updatedUserProfile.getDrinkingFrequency()) - .smokingStatus(updatedUserProfile.getSmokingStatus()) - .height(updatedUserProfile.getHeight()) - .pet(updatedUserProfile.getPet()) - .religion(updatedUserProfile.getReligion()) - .childPlan(updatedUserProfile.getChildPlan()) - .mbti(updatedUserProfile.getMbti()) - .build(); + + @Transactional + public void deleteUser(Long userId) { + User user = repository.findByIdWithProfile(userId) + .orElseThrow(() -> new EntityNotFoundException("User with id " + userId + " not found")); + + repository.delete(user); } + /* + // 현재 사용자의 거주 지역을 기준으로 같은 지역 사용자 찾기 @Transactional(readOnly = true) public List getUsersBySamePlace(Long myBasicInfoId) { diff --git a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java index 04fc251..1ff5706 100644 --- a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java +++ b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java @@ -1,12 +1,12 @@ package project.backend.user.dto; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import java.time.LocalDate; @Getter -@NoArgsConstructor +@AllArgsConstructor public class SignUpRequestDTO { // 기본정보 diff --git a/src/main/java/project/backend/user/dto/UserProfileDTO.java b/src/main/java/project/backend/user/dto/UserProfileDTO.java new file mode 100644 index 0000000..48e2828 --- /dev/null +++ b/src/main/java/project/backend/user/dto/UserProfileDTO.java @@ -0,0 +1,17 @@ +package project.backend.user.dto; + +import lombok.Getter; + +@Getter +public class UserProfileDTO { + + private UserEnums.Job job; + private String region; + private UserEnums.DrinkingFrequency drinkingFrequency; + private UserEnums.SmokingStatus smokingStatus; + private Integer height; + private UserEnums.PetPreference petPreference; + private UserEnums.Religion religion; + private UserEnums.ContactFrequency contactFrequency; + private UserEnums.Mbti mbti; +} diff --git a/src/main/java/project/backend/user/dto/UserResponseDTO.java b/src/main/java/project/backend/user/dto/UserResponseDTO.java index 68435c7..4d1f60d 100644 --- a/src/main/java/project/backend/user/dto/UserResponseDTO.java +++ b/src/main/java/project/backend/user/dto/UserResponseDTO.java @@ -1,10 +1,10 @@ package project.backend.user.dto; -import lombok.Builder; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@Builder +@AllArgsConstructor public class UserResponseDTO { private Long id; diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 35f79e4..40ed526 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -3,12 +3,13 @@ import jakarta.persistence.*; import lombok.*; import project.backend.user.dto.UserEnums; +import project.backend.user.dto.UserProfileDTO; @Entity @Table(name = "user_profiles") @Getter @NoArgsConstructor -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder public class UserProfile { @@ -21,7 +22,7 @@ public class UserProfile { @JoinColumn(name = "user_id", nullable = false) private User user; - private String profileImagePath; + //private String profileImagePath; @Enumerated(EnumType.STRING) private UserEnums.Job job; @@ -39,6 +40,7 @@ public class UserProfile { @Enumerated(EnumType.STRING) private UserEnums.PetPreference petPreference; + @Setter @Enumerated(EnumType.STRING) private UserEnums.Religion religion; @@ -48,4 +50,50 @@ public class UserProfile { @Enumerated(EnumType.STRING) private UserEnums.Mbti mbti; + public void updateUserProfile(UserProfileDTO userProfileDTO) { + if (userProfileDTO.getJob() != null) { + this.job = userProfileDTO.getJob(); + } + if (userProfileDTO.getRegion() != null) { + this.region = userProfileDTO.getRegion(); + } + if (userProfileDTO.getDrinkingFrequency() != null) { + this.drinkingFrequency = userProfileDTO.getDrinkingFrequency(); + } + if (userProfileDTO.getSmokingStatus() != null) { + this.smokingStatus = userProfileDTO.getSmokingStatus(); + } + if (userProfileDTO.getHeight() != null) { + this.height = userProfileDTO.getHeight(); + } + if (userProfileDTO.getPetPreference() != null) { + this.petPreference = userProfileDTO.getPetPreference(); + } + if (userProfileDTO.getReligion() != null) { + this.religion = userProfileDTO.getReligion(); + } + if (userProfileDTO.getContactFrequency() != null) { + this.contactFrequency = userProfileDTO.getContactFrequency(); + } + if (userProfileDTO.getMbti() != null) { + this.mbti = userProfileDTO.getMbti(); + } + } + + @Override + public String toString() { + return "UserProfile{" + + "id=" + id + + ", user=" + user + + ", job=" + job + + ", region='" + region + '\'' + + ", drinkingFrequency=" + drinkingFrequency + + ", smokingStatus=" + smokingStatus + + ", height=" + height + + ", petPreference=" + petPreference + + ", religion=" + religion + + ", contactFrequency=" + contactFrequency + + ", mbti=" + mbti + + '}'; + } } \ No newline at end of file From 55c7422915139bbed7d76bd5144c22dfed394484 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Mon, 3 Nov 2025 21:21:51 +0900 Subject: [PATCH 15/32] =?UTF-8?q?feat=20:=20=EC=84=B1=EC=A0=95=EC=B2=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개인정보에 성 정체성 카테고리 추가 --- .../backend/mypage/dto/MyPageDisplayDTO.java | 2 ++ .../project/backend/user/UserService.java | 1 + .../backend/user/dto/SignUpRequestDTO.java | 1 + .../project/backend/user/dto/UserEnums.java | 4 ++++ .../backend/user/dto/UserProfileDTO.java | 1 + .../backend/user/entity/UserProfile.java | 23 +++++-------------- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java index d219c23..a3efc16 100644 --- a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java +++ b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java @@ -18,6 +18,7 @@ public class MyPageDisplayDTO { private LocalDate birthDate; // UserProfile 정보 + private UserEnums.SexualOrientation sexualOrientation; private UserEnums.Job job; private String region; private UserEnums.DrinkingFrequency drinkingFrequency; @@ -36,6 +37,7 @@ public MyPageDisplayDTO(User user, UserProfile profile) { this.birthDate = user.getBirthDate(); if (profile != null) { + this.sexualOrientation = profile.getSexualOrientation(); this.job = profile.getJob(); this.region = profile.getRegion(); this.drinkingFrequency = profile.getDrinkingFrequency(); diff --git a/src/main/java/project/backend/user/UserService.java b/src/main/java/project/backend/user/UserService.java index 017a37c..ca91092 100644 --- a/src/main/java/project/backend/user/UserService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -30,6 +30,7 @@ public class UserService { @Transactional public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { UserProfile userProfile = UserProfile.builder() + .sexualOrientation(requestDTO.getSexualorientation()) .job(requestDTO.getJob()) .region(requestDTO.getRegion()) .drinkingFrequency(requestDTO.getDrinkingFrequency()) diff --git a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java index 1ff5706..741b397 100644 --- a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java +++ b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java @@ -15,6 +15,7 @@ public class SignUpRequestDTO { private UserEnums.Gender gender; // 상세정보 + private UserEnums.SexualOrientation sexualorientation; private UserEnums.Job job; private String region; private UserEnums.DrinkingFrequency drinkingFrequency; diff --git a/src/main/java/project/backend/user/dto/UserEnums.java b/src/main/java/project/backend/user/dto/UserEnums.java index a3c9cb8..e3ccd7b 100644 --- a/src/main/java/project/backend/user/dto/UserEnums.java +++ b/src/main/java/project/backend/user/dto/UserEnums.java @@ -7,6 +7,10 @@ public enum Gender { FEMALE } + public enum SexualOrientation { + STRAIGHT, HOMOSEXUAL + } + public enum Job { UNEMPLOYED, STUDENT, diff --git a/src/main/java/project/backend/user/dto/UserProfileDTO.java b/src/main/java/project/backend/user/dto/UserProfileDTO.java index 48e2828..badeea2 100644 --- a/src/main/java/project/backend/user/dto/UserProfileDTO.java +++ b/src/main/java/project/backend/user/dto/UserProfileDTO.java @@ -5,6 +5,7 @@ @Getter public class UserProfileDTO { + private UserEnums.SexualOrientation sexualorientation; private UserEnums.Job job; private String region; private UserEnums.DrinkingFrequency drinkingFrequency; diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 40ed526..027d78f 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -24,6 +24,9 @@ public class UserProfile { //private String profileImagePath; + @Enumerated(EnumType.STRING) + private UserEnums.SexualOrientation sexualOrientation; + @Enumerated(EnumType.STRING) private UserEnums.Job job; @@ -51,6 +54,9 @@ public class UserProfile { private UserEnums.Mbti mbti; public void updateUserProfile(UserProfileDTO userProfileDTO) { + if (userProfileDTO.getSexualorientation() != null) { + this.sexualOrientation = userProfileDTO.getSexualorientation(); + } if (userProfileDTO.getJob() != null) { this.job = userProfileDTO.getJob(); } @@ -79,21 +85,4 @@ public void updateUserProfile(UserProfileDTO userProfileDTO) { this.mbti = userProfileDTO.getMbti(); } } - - @Override - public String toString() { - return "UserProfile{" + - "id=" + id + - ", user=" + user + - ", job=" + job + - ", region='" + region + '\'' + - ", drinkingFrequency=" + drinkingFrequency + - ", smokingStatus=" + smokingStatus + - ", height=" + height + - ", petPreference=" + petPreference + - ", religion=" + religion + - ", contactFrequency=" + contactFrequency + - ", mbti=" + mbti + - '}'; - } } \ No newline at end of file From 03caae4a72965838282ee16202f3c0ad3f864490 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Tue, 4 Nov 2025 11:37:23 +0900 Subject: [PATCH 16/32] =?UTF-8?q?feat=20:=20=EC=A7=80=EC=97=AD=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/mypage/dto/MyPageDisplayDTO.java | 2 +- .../backend/user/dto/SignUpRequestDTO.java | 2 +- .../project/backend/user/dto/UserEnums.java | 21 +++++++++++++++++++ .../backend/user/dto/UserProfileDTO.java | 2 +- .../backend/user/entity/UserProfile.java | 3 ++- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java index a3efc16..7072e1b 100644 --- a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java +++ b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java @@ -20,7 +20,7 @@ public class MyPageDisplayDTO { // UserProfile 정보 private UserEnums.SexualOrientation sexualOrientation; private UserEnums.Job job; - private String region; + private UserEnums.region region; private UserEnums.DrinkingFrequency drinkingFrequency; private UserEnums.SmokingStatus smokingStatus; private Integer height; diff --git a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java index 741b397..b2b9a5a 100644 --- a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java +++ b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java @@ -17,7 +17,7 @@ public class SignUpRequestDTO { // 상세정보 private UserEnums.SexualOrientation sexualorientation; private UserEnums.Job job; - private String region; + private UserEnums.region region; private UserEnums.DrinkingFrequency drinkingFrequency; private UserEnums.SmokingStatus smokingStatus; private Integer height; diff --git a/src/main/java/project/backend/user/dto/UserEnums.java b/src/main/java/project/backend/user/dto/UserEnums.java index e3ccd7b..73936c4 100644 --- a/src/main/java/project/backend/user/dto/UserEnums.java +++ b/src/main/java/project/backend/user/dto/UserEnums.java @@ -17,6 +17,27 @@ public enum Job { EMPLOYEE } + public enum region { + SEOUL, + Seoul, + Gyeonggi_do, + Incheon, + Busan, + Daegu, + Gwangju, + Daejeon, + Ulsan, + Sejong, + Gangwon_do, + Chungcheongbuk_do, + Chungcheongnam_do, + Jeollabuk_do, + Jeollanam_do, + Gyeongsangbuk_do, + Gyeongsangnam_do, + Jeju_do, + } + public enum DrinkingFrequency { NONE, ONCE_OR_TWICE_PER_WEEK, diff --git a/src/main/java/project/backend/user/dto/UserProfileDTO.java b/src/main/java/project/backend/user/dto/UserProfileDTO.java index badeea2..e0134c6 100644 --- a/src/main/java/project/backend/user/dto/UserProfileDTO.java +++ b/src/main/java/project/backend/user/dto/UserProfileDTO.java @@ -7,7 +7,7 @@ public class UserProfileDTO { private UserEnums.SexualOrientation sexualorientation; private UserEnums.Job job; - private String region; + private UserEnums.region region; private UserEnums.DrinkingFrequency drinkingFrequency; private UserEnums.SmokingStatus smokingStatus; private Integer height; diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 027d78f..0cc4434 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -30,7 +30,8 @@ public class UserProfile { @Enumerated(EnumType.STRING) private UserEnums.Job job; - private String region; + @Enumerated(EnumType.STRING) + private UserEnums.region region; @Enumerated(EnumType.STRING) private UserEnums.DrinkingFrequency drinkingFrequency; From 14d39ac0ba7d36cb3641b9813a8ed081adc781da Mon Sep 17 00:00:00 2001 From: audwns03 Date: Wed, 5 Nov 2025 20:58:14 +0900 Subject: [PATCH 17/32] =?UTF-8?q?feat=20:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/backend/mypage/dto/MyPageDisplayDTO.java | 2 ++ src/main/java/project/backend/user/UserService.java | 1 + src/main/java/project/backend/user/dto/UserProfileDTO.java | 1 + src/main/java/project/backend/user/entity/UserProfile.java | 5 +++++ 4 files changed, 9 insertions(+) diff --git a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java index 7072e1b..2f7f780 100644 --- a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java +++ b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java @@ -28,6 +28,7 @@ public class MyPageDisplayDTO { private UserEnums.Religion religion; private UserEnums.ContactFrequency contactFrequency; private UserEnums.Mbti mbti; + private String introduction; @Builder public MyPageDisplayDTO(User user, UserProfile profile) { @@ -47,6 +48,7 @@ public MyPageDisplayDTO(User user, UserProfile profile) { this.religion = profile.getReligion(); this.contactFrequency = profile.getContactFrequency(); this.mbti = profile.getMbti(); + this.introduction = profile.getIntroduction(); } } diff --git a/src/main/java/project/backend/user/UserService.java b/src/main/java/project/backend/user/UserService.java index ca91092..22cf852 100644 --- a/src/main/java/project/backend/user/UserService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -40,6 +40,7 @@ public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { .religion(requestDTO.getReligion()) .contactFrequency(requestDTO.getContactFrequency()) .mbti(requestDTO.getMbti()) + .introduction(requestDTO.getIntroduction()) .build(); User user = User.builder() diff --git a/src/main/java/project/backend/user/dto/UserProfileDTO.java b/src/main/java/project/backend/user/dto/UserProfileDTO.java index e0134c6..3f6d381 100644 --- a/src/main/java/project/backend/user/dto/UserProfileDTO.java +++ b/src/main/java/project/backend/user/dto/UserProfileDTO.java @@ -15,4 +15,5 @@ public class UserProfileDTO { private UserEnums.Religion religion; private UserEnums.ContactFrequency contactFrequency; private UserEnums.Mbti mbti; + private String introduction; } diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 0cc4434..19ee9c5 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -54,6 +54,8 @@ public class UserProfile { @Enumerated(EnumType.STRING) private UserEnums.Mbti mbti; + private String introduction; + public void updateUserProfile(UserProfileDTO userProfileDTO) { if (userProfileDTO.getSexualorientation() != null) { this.sexualOrientation = userProfileDTO.getSexualorientation(); @@ -85,5 +87,8 @@ public void updateUserProfile(UserProfileDTO userProfileDTO) { if (userProfileDTO.getMbti() != null) { this.mbti = userProfileDTO.getMbti(); } + if (userProfileDTO.getIntroduction() != null) { + this.introduction = userProfileDTO.getIntroduction(); + } } } \ No newline at end of file From cedbf245a1dd1759c1c1097f93c7ad75a855c461 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 8 Nov 2025 01:09:06 +0900 Subject: [PATCH 18/32] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=A7=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/mypage/MyPageController.java | 14 +- .../project/backend/mypage/MyPageService.java | 15 +- .../backend/mypage/dto/MyPageDisplayDTO.java | 2 + .../backend/user/UserInfoController.java | 19 ++- .../project/backend/user/UserService.java | 142 +++++++----------- .../java/project/backend/user/WebConfig.java | 7 +- .../project/backend/user/dto/UserEnums.java | 33 ++-- .../backend/user/entity/UserProfile.java | 3 +- 8 files changed, 113 insertions(+), 122 deletions(-) diff --git a/src/main/java/project/backend/mypage/MyPageController.java b/src/main/java/project/backend/mypage/MyPageController.java index 97077f3..cd65eab 100644 --- a/src/main/java/project/backend/mypage/MyPageController.java +++ b/src/main/java/project/backend/mypage/MyPageController.java @@ -1,7 +1,10 @@ package project.backend.mypage; +import java.io.IOException; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; import project.backend.mypage.dto.MyPageDisplayDTO; @@ -32,7 +35,16 @@ public ResponseEntity updateProfile( return ResponseEntity.ok().build(); } - + // 마이페이지 프로필 이미지 수정 + @PatchMapping(value = "/{userId}/profile-image", consumes = "multipart/form-data") + public ResponseEntity updateProfileImage( + @PathVariable("userId") Long userId, + @RequestPart("profileImage") MultipartFile profileImage) throws IOException { + String imagePath = myPageService.updateProfileImage(userId, profileImage); + + return ResponseEntity.ok(imagePath); + } + // 회원 탈퇴 @DeleteMapping("/{userId}") public ResponseEntity deleteUser(@PathVariable("userId") Long userId) { diff --git a/src/main/java/project/backend/mypage/MyPageService.java b/src/main/java/project/backend/mypage/MyPageService.java index 730a528..bbeed3c 100644 --- a/src/main/java/project/backend/mypage/MyPageService.java +++ b/src/main/java/project/backend/mypage/MyPageService.java @@ -1,11 +1,12 @@ package project.backend.mypage; -import java.time.LocalDate; -import java.time.Period; +import java.io.IOException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + import lombok.RequiredArgsConstructor; import project.backend.mypage.dto.MyPageDisplayDTO; import project.backend.user.UserService; @@ -29,14 +30,16 @@ public void editProfile(Long userId, UserProfileDTO userProfileDTO) { userService.updateUserProfileInfo(userId, userProfileDTO); } + // user 프로필 사진 수정 + @Transactional + public String updateProfileImage(Long userId, MultipartFile profileImage) throws IOException { + return userService.updateUserProfileImage(userId, profileImage); + } + // 회원 탈퇴 @Transactional public void deleteUser(Long userId) { userService.deleteUser(userId); } - // 나이 계산 (만 나이) 아직 사용 x - private Integer calculateAge(LocalDate birthDate) { - return (birthDate == null) ? null : Period.between(birthDate, LocalDate.now()).getYears(); - } } \ No newline at end of file diff --git a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java index 2f7f780..ea892b4 100644 --- a/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java +++ b/src/main/java/project/backend/mypage/dto/MyPageDisplayDTO.java @@ -29,6 +29,7 @@ public class MyPageDisplayDTO { private UserEnums.ContactFrequency contactFrequency; private UserEnums.Mbti mbti; private String introduction; + private String profileImagePath; @Builder public MyPageDisplayDTO(User user, UserProfile profile) { @@ -49,6 +50,7 @@ public MyPageDisplayDTO(User user, UserProfile profile) { this.contactFrequency = profile.getContactFrequency(); this.mbti = profile.getMbti(); this.introduction = profile.getIntroduction(); + this.profileImagePath = profile.getProfileImagePath(); } } diff --git a/src/main/java/project/backend/user/UserInfoController.java b/src/main/java/project/backend/user/UserInfoController.java index 12604dd..22a183e 100644 --- a/src/main/java/project/backend/user/UserInfoController.java +++ b/src/main/java/project/backend/user/UserInfoController.java @@ -1,11 +1,12 @@ package project.backend.user; +import java.io.IOException; + import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; import project.backend.user.dto.SignUpRequestDTO; @@ -18,10 +19,12 @@ public class UserInfoController { private final UserService userService; - // 기본 정보 저장 - @PostMapping("/signup") - public ResponseEntity signUp(@RequestBody SignUpRequestDTO requestDTO) { - UserResponseDTO response = userService.registerNewUser(requestDTO); + // 회원가입 (사진파일 + json 상세 정보) + @PostMapping(value = "/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity signUp( + @ModelAttribute SignUpRequestDTO requestDTO, + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage) throws IOException { + UserResponseDTO response = userService.registerNewUser(requestDTO, profileImage); return ResponseEntity.status(HttpStatus.CREATED).body(response); } diff --git a/src/main/java/project/backend/user/UserService.java b/src/main/java/project/backend/user/UserService.java index 22cf852..9d703c0 100644 --- a/src/main/java/project/backend/user/UserService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -1,16 +1,20 @@ package project.backend.user; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; -import org.springframework.transaction.interceptor.TransactionAspectSupport; import project.backend.mypage.dto.MyPageDisplayDTO; import project.backend.user.dto.SignUpRequestDTO; -import project.backend.user.dto.UserEnums; import project.backend.user.dto.UserProfileDTO; import project.backend.user.dto.UserResponseDTO; import project.backend.user.entity.User; @@ -28,7 +32,7 @@ public class UserService { //회원가입 @Transactional - public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { + public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO, MultipartFile profileImage) throws IOException { UserProfile userProfile = UserProfile.builder() .sexualOrientation(requestDTO.getSexualorientation()) .job(requestDTO.getJob()) @@ -43,6 +47,12 @@ public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { .introduction(requestDTO.getIntroduction()) .build(); + // 프로필 이미지 저장 + if (profileImage != null && !profileImage.isEmpty()) { + String imagePath = saveProfileImage(profileImage); + userProfile.setProfileImagePath(imagePath); + } + User user = User.builder() .name(requestDTO.getName()) .gender(requestDTO.getGender()) @@ -55,7 +65,7 @@ public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO) { return new UserResponseDTO(user.getId(), user.getName()); //과연 필요할까..??일단 넣음 } - + // 전체 사용자 정보 조회 public MyPageDisplayDTO getUserInfo(Long userId) { User user = repository.findByIdWithProfile(userId) @@ -75,117 +85,73 @@ public void updateUserProfileInfo(Long userId, UserProfileDTO userProfileDTO) { } + //회원 탈퇴 @Transactional public void deleteUser(Long userId) { User user = repository.findByIdWithProfile(userId) .orElseThrow(() -> new EntityNotFoundException("User with id " + userId + " not found")); - repository.delete(user); - } - - /* - - // 현재 사용자의 거주 지역을 기준으로 같은 지역 사용자 찾기 - @Transactional(readOnly = true) - public List getUsersBySamePlace(Long myBasicInfoId) { - // 내 상세정보 가져오기 - UserProfile myDetail = detailInfoRepository.findByBasicInfoId(myBasicInfoId) - .orElseThrow(() -> new IllegalArgumentException("상세정보를 찾을 수 없습니다.")); - - String myPlace = myDetail.getPlace(); - - // 같은 지역 사용자 전체 조회 - List usersInSamePlace = detailInfoRepository.findByPlace(myPlace); - - return usersInSamePlace.stream() - .filter(detail -> !detail.getUser().getId().equals(myBasicInfoId)) // 자신 제외 - .map(detail -> { - User basic = detail.getUser(); - - return UserInfoResponseDTO.builder() - .basicInfoId(basic.getId()) - .name(basic.getName()) - .gender(basic.getGender()) - .birthDate(basic.getBirthDate()) - .detailInfoId(detail.getId()) - .profileImagePath(detail.getProfileImagePath()) - .place(detail.getPlace()) - .drinkingFrequency(detail.getDrinkingFrequency()) - .smokingStatus(detail.getSmokingStatus()) - .height(detail.getHeight()) - .pet(detail.getPet()) - .religion(detail.getReligion()) - .childPlan(detail.getChildPlan()) - .mbti(detail.getMbti()) - .build(); - }) - .toList(); - } - - // 회원 탈퇴 (MyPageService에서 위임 호출) - public void deleteUser(Long userId) { - User user = repository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId)); - - // 프로필 이미지 삭제 - UserProfile userProfile = detailInfoRepository.findByBasicInfoId(userId).orElse(null); + UserProfile userProfile = user.getUserProfile(); if (userProfile != null && userProfile.getProfileImagePath() != null) { deleteProfileImage(userProfile.getProfileImagePath()); } - - // DB에서 삭제 (Cascade 설정으로 DetailInfo도 함께 삭제됨) + repository.delete(user); - - log.info("회원 탈퇴 완료 - 사용자 ID: {}", userId); } - + + //프로필 이미지 수정 + @Transactional + public String updateUserProfileImage(Long userId, MultipartFile newImage) throws IOException { + if (newImage == null || newImage.isEmpty()) { + throw new IllegalArgumentException("Profile image file must not be empty"); + } + + User user = repository.findByIdWithProfile(userId) + .orElseThrow(() -> new EntityNotFoundException("User with id " + userId + " not found")); + + UserProfile userProfile = user.getUserProfile(); + if (userProfile == null) { + throw new EntityNotFoundException("User profile not found for user id " + userId); + } + + if (userProfile.getProfileImagePath() != null) { + deleteProfileImage(userProfile.getProfileImagePath()); + } + + String newPath = saveProfileImage(newImage); + userProfile.setProfileImagePath(newPath); + + return newPath; + } + // 프로필 이미지 저장 private String saveProfileImage(MultipartFile file) throws IOException { - // 업로드 디렉터리 생성 Path uploadPath = Paths.get(uploadDir); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } - - // 고유한 파일명 생성 + String originalFilename = file.getOriginalFilename(); - String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String extension = ""; + if (originalFilename != null && originalFilename.lastIndexOf('.') != -1) { + extension = originalFilename.substring(originalFilename.lastIndexOf('.')); + } String fileName = UUID.randomUUID().toString() + extension; - - // 파일 저장 + Path filePath = uploadPath.resolve(fileName); Files.copy(file.getInputStream(), filePath); - + return "/uploads/profile/" + fileName; } - + // 프로필 이미지 삭제 private void deleteProfileImage(String imagePath) { try { - String fileName = imagePath.substring(imagePath.lastIndexOf("/") + 1); + String fileName = imagePath.substring(imagePath.lastIndexOf('/') + 1); Path filePath = Paths.get(uploadDir).resolve(fileName); Files.deleteIfExists(filePath); } catch (Exception e) { - log.error("프로필 이미지 삭제 실패: " + imagePath, e); + e.printStackTrace(); } } - - // MyPageService 위임 호출용 - // 기존 프로필 이미지를 삭제하고 새로운 프로필 이미지 저장 후 경로 반환 - public String updateProfileImage(Long userId, MultipartFile newImage) throws IOException { - UserProfile userProfile = detailInfoRepository.findByBasicInfoId(userId) - .orElseThrow(() -> new EntityNotFoundException("상세 정보를 찾을 수 없습니다. ID: " + userId)); - - // 기존 이미지 삭제 - if (userProfile.getProfileImagePath() != null) { - deleteProfileImage(userProfile.getProfileImagePath()); - } - - // 새 이미지 저장 - String newPath = saveProfileImage(newImage); - userProfile.setProfileImagePath(newPath); - detailInfoRepository.save(userProfile); - - return newPath; - }*/ } \ No newline at end of file diff --git a/src/main/java/project/backend/user/WebConfig.java b/src/main/java/project/backend/user/WebConfig.java index 3497912..c57888f 100644 --- a/src/main/java/project/backend/user/WebConfig.java +++ b/src/main/java/project/backend/user/WebConfig.java @@ -1,14 +1,19 @@ package project.backend.user; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { + + @Value("${file.upload-dir:uploads/profile}") + private String uploadDir; + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/profile/**") - .addResourceLocations("file:uploads/profile/"); + .addResourceLocations("file:" + uploadDir + "/"); } } \ No newline at end of file diff --git a/src/main/java/project/backend/user/dto/UserEnums.java b/src/main/java/project/backend/user/dto/UserEnums.java index 73936c4..ed82604 100644 --- a/src/main/java/project/backend/user/dto/UserEnums.java +++ b/src/main/java/project/backend/user/dto/UserEnums.java @@ -19,23 +19,22 @@ public enum Job { public enum region { SEOUL, - Seoul, - Gyeonggi_do, - Incheon, - Busan, - Daegu, - Gwangju, - Daejeon, - Ulsan, - Sejong, - Gangwon_do, - Chungcheongbuk_do, - Chungcheongnam_do, - Jeollabuk_do, - Jeollanam_do, - Gyeongsangbuk_do, - Gyeongsangnam_do, - Jeju_do, + GYEONGGI_DO, + INCHEON, + BUSAN, + DAEGU, + GWANGJU, + DAEJEON, + ULSAN, + SEJONG, + GANGWON_DO, + CHUNGCHEONGBUK_DO, + CHUNGCHEONGNAM_DO, + JEOLLABUK_DO, + JEOLLANAM_DO, + GYEONGSANGBUK_DO, + GYEONGSANGNAM_DO, + JEJU_DO, } public enum DrinkingFrequency { diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 19ee9c5..0da3f1d 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -22,7 +22,8 @@ public class UserProfile { @JoinColumn(name = "user_id", nullable = false) private User user; - //private String profileImagePath; + @Setter + private String profileImagePath; @Enumerated(EnumType.STRING) private UserEnums.SexualOrientation sexualOrientation; From 347cecb208e2a13e2957cb15f5acad7e0aca0d45 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 8 Nov 2025 17:50:27 +0900 Subject: [PATCH 19/32] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=A3=BC=EA=B6=81?= =?UTF-8?q?=ED=95=A9=20api=20=EC=97=B0=EA=B2=B0=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사주궁합 반환 서버의 값을 보내기 + 받는 코드 추가 --- .../project/backend/pythonapi/PersonInfo.java | 5 ++++ .../backend/pythonapi/SajuController.java | 21 +++++++++++++++ .../backend/pythonapi/SajuRequest.java | 13 ++++++++++ .../backend/pythonapi/SajuResponse.java | 26 +++++++++++++++++++ .../backend/pythonapi/SajuService.java | 23 ++++++++++++++++ .../backend/pythonapi/WebClientConfig.java | 23 ++++++++++++++++ 6 files changed, 111 insertions(+) create mode 100644 src/main/java/project/backend/pythonapi/PersonInfo.java create mode 100644 src/main/java/project/backend/pythonapi/SajuController.java create mode 100644 src/main/java/project/backend/pythonapi/SajuRequest.java create mode 100644 src/main/java/project/backend/pythonapi/SajuResponse.java create mode 100644 src/main/java/project/backend/pythonapi/SajuService.java create mode 100644 src/main/java/project/backend/pythonapi/WebClientConfig.java diff --git a/src/main/java/project/backend/pythonapi/PersonInfo.java b/src/main/java/project/backend/pythonapi/PersonInfo.java new file mode 100644 index 0000000..8a5f7d5 --- /dev/null +++ b/src/main/java/project/backend/pythonapi/PersonInfo.java @@ -0,0 +1,5 @@ +package project.backend.pythonapi; + +public record PersonInfo(int year, int month, int day, int gender) { + +} \ No newline at end of file diff --git a/src/main/java/project/backend/pythonapi/SajuController.java b/src/main/java/project/backend/pythonapi/SajuController.java new file mode 100644 index 0000000..1cfd440 --- /dev/null +++ b/src/main/java/project/backend/pythonapi/SajuController.java @@ -0,0 +1,21 @@ +package project.backend.pythonapi; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/api/match") +@RequiredArgsConstructor +public class SajuController { + + private final SajuService matchService; + + @PostMapping + public Mono getMatchScore(@RequestBody SajuRequest matchRequest) { + return matchService.getMatchResult(matchRequest); + } +} diff --git a/src/main/java/project/backend/pythonapi/SajuRequest.java b/src/main/java/project/backend/pythonapi/SajuRequest.java new file mode 100644 index 0000000..6401282 --- /dev/null +++ b/src/main/java/project/backend/pythonapi/SajuRequest.java @@ -0,0 +1,13 @@ +package project.backend.pythonapi; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SajuRequest { + + private PersonInfo person1; + + private PersonInfo person2; +} diff --git a/src/main/java/project/backend/pythonapi/SajuResponse.java b/src/main/java/project/backend/pythonapi/SajuResponse.java new file mode 100644 index 0000000..381c6e9 --- /dev/null +++ b/src/main/java/project/backend/pythonapi/SajuResponse.java @@ -0,0 +1,26 @@ +package project.backend.pythonapi; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record SajuResponse( + @JsonProperty("original_score") + double originalScore, + + @JsonProperty("final_score") + double finalScore, + + @JsonProperty("stress_score") + double stressScore, + + @JsonProperty("person1_sal_analysis") + String person1SalAnalysis, + + @JsonProperty("person2_sal_analysis") + String person2SalAnalysis, + + @JsonProperty("match_analysis") + String matchAnalysis, + + String error +) { +} \ No newline at end of file diff --git a/src/main/java/project/backend/pythonapi/SajuService.java b/src/main/java/project/backend/pythonapi/SajuService.java new file mode 100644 index 0000000..d7e9e1b --- /dev/null +++ b/src/main/java/project/backend/pythonapi/SajuService.java @@ -0,0 +1,23 @@ +package project.backend.pythonapi; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +public class SajuService { + + private final WebClient webClient; + + public Mono getMatchResult(SajuRequest request) { + + return webClient.post() + .uri("/match") + .bodyValue(request) + .retrieve() + .bodyToMono(SajuResponse.class); + + } +} diff --git a/src/main/java/project/backend/pythonapi/WebClientConfig.java b/src/main/java/project/backend/pythonapi/WebClientConfig.java new file mode 100644 index 0000000..994490f --- /dev/null +++ b/src/main/java/project/backend/pythonapi/WebClientConfig.java @@ -0,0 +1,23 @@ +package project.backend.pythonapi; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Value("${api.python.baseUrl}") + private String baseUrl; + + @Bean + public WebClient webClient() { + return WebClient.builder() + .baseUrl(baseUrl) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + } +} From e2e6cfc634fc3ef997f8907135a142d037f30b4d Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 8 Nov 2025 21:47:15 +0900 Subject: [PATCH 20/32] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=AD=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매칭하기 관련 클래스 추가 -매칭 시 지역+ 성 정체성이 같은 사람중에서 랜덤 매칭 -매칭된 사람과 pythonapi에 정보 전송 후 궁합점수 반환 -최종적으로 궁합점수 + 상대방 기본,상세 정보 반환 *오타 수정 --- .../backend/matching/MatchingController.java | 17 +++ .../backend/matching/MatchingResultDTO.java | 15 +++ .../backend/matching/MatchingService.java | 108 ++++++++++++++++++ .../backend/pythonapi/SajuRequest.java | 4 +- .../project/backend/user/UserRepository.java | 24 ++++ .../project/backend/user/UserService.java | 2 +- .../backend/user/dto/SignUpRequestDTO.java | 2 +- .../backend/user/dto/UserProfileDTO.java | 2 +- .../backend/user/entity/UserProfile.java | 4 +- 9 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 src/main/java/project/backend/matching/MatchingController.java create mode 100644 src/main/java/project/backend/matching/MatchingResultDTO.java create mode 100644 src/main/java/project/backend/matching/MatchingService.java diff --git a/src/main/java/project/backend/matching/MatchingController.java b/src/main/java/project/backend/matching/MatchingController.java new file mode 100644 index 0000000..404ff51 --- /dev/null +++ b/src/main/java/project/backend/matching/MatchingController.java @@ -0,0 +1,17 @@ +package project.backend.matching; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/matching") +@RequiredArgsConstructor +public class MatchingController { + + private final MatchingService matchingService; + + @GetMapping("/{userId}") + public MatchingResultDTO getMatchingResult(@PathVariable("userId") Long userId) throws Exception { + return matchingService.getMatchingResult(userId); + } +} diff --git a/src/main/java/project/backend/matching/MatchingResultDTO.java b/src/main/java/project/backend/matching/MatchingResultDTO.java new file mode 100644 index 0000000..a4c3746 --- /dev/null +++ b/src/main/java/project/backend/matching/MatchingResultDTO.java @@ -0,0 +1,15 @@ +package project.backend.matching; + +import lombok.Builder; +import lombok.Getter; +import project.backend.mypage.dto.MyPageDisplayDTO; +import project.backend.pythonapi.SajuResponse; + +@Getter +@Builder +public class MatchingResultDTO { + + private final SajuResponse sajuResponse; + + private final MyPageDisplayDTO personInfo; +} diff --git a/src/main/java/project/backend/matching/MatchingService.java b/src/main/java/project/backend/matching/MatchingService.java new file mode 100644 index 0000000..ce8934e --- /dev/null +++ b/src/main/java/project/backend/matching/MatchingService.java @@ -0,0 +1,108 @@ +package project.backend.matching; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import project.backend.mypage.dto.MyPageDisplayDTO; +import project.backend.openai.OpenAiService; +import project.backend.pythonapi.PersonInfo; +import project.backend.pythonapi.SajuRequest; +import project.backend.pythonapi.SajuResponse; +import project.backend.pythonapi.SajuService; +import project.backend.user.UserRepository; +import project.backend.user.dto.UserEnums; +import project.backend.user.entity.User; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; +import java.util.Random; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MatchingService { + + private final SajuService sajuService; + private final UserRepository userRepository; + private final OpenAiService openAiService; + + //전체 매칭하기 기능 반환 + public MatchingResultDTO getMatchingResult(Long userId) throws Exception { + + Optional userOptional = userRepository.findById(userId); + if (userOptional.isEmpty()) { + throw new Exception("User not found"); + } + User user = userOptional.get(); + int userGender = 0; + if (user.getGender() == UserEnums.Gender.MALE) { + userGender = 1; + } + + User randomUser = randomUser(user); + int randomUserGender = 0; + if (randomUser.getGender() == UserEnums.Gender.MALE) { + randomUserGender = 1; + } + + SajuRequest sajuRequest = new SajuRequest( + new PersonInfo( + user.getBirthDate().getYear(), + user.getBirthDate().getMonthValue(), + user.getBirthDate().getDayOfMonth(), + userGender), + new PersonInfo( + randomUser.getBirthDate().getYear(), + randomUser.getBirthDate().getMonthValue(), + randomUser.getBirthDate().getDayOfMonth(), + randomUserGender) + ); + Mono sajuResponseMono = getSajuResponse(sajuRequest); + SajuResponse sajuResponse = sajuResponseMono.block(); + + MyPageDisplayDTO myPageDisplayDTO = new MyPageDisplayDTO(randomUser, randomUser.getUserProfile()); + + return MatchingResultDTO.builder().sajuResponse(sajuResponse).personInfo(myPageDisplayDTO).build(); + } + + //랜덤으로 상대방 불러오기(지역 + 씹게이 판별) + private User randomUser(User myUser) { + UserEnums.region region = myUser.getUserProfile().getRegion(); + UserEnums.SexualOrientation sexualOrientation = myUser.getUserProfile().getSexualOrientation(); + UserEnums.Gender myGender = myUser.getGender(); + + List matchingUsers; + + // STRAIGHT인 경우 성별이 반대인 사용자들만 조회 + if (sexualOrientation == UserEnums.SexualOrientation.STRAIGHT) { + matchingUsers = userRepository.findMatchingUsersForStraight( + region, + sexualOrientation, + myUser.getId(), + myGender + ); + } else { + // HOMOSEXUAL인 경우 같은 sexualOrientation을 가진 모든 사용자 조회 + matchingUsers = userRepository.findMatchingUsersByRegionAndOrientation( + region, + sexualOrientation, + myUser.getId() + ); + } + + if (matchingUsers.isEmpty()) { + throw new RuntimeException("매칭 가능한 사용자가 없습니다."); + } + + // 랜덤으로 하나 선택 + Random random = new Random(); + int randomIndex = random.nextInt(matchingUsers.size()); + return matchingUsers.get(randomIndex); + } + + //pythonApi 에서 궁합점수 반환 + private Mono getSajuResponse(SajuRequest sajuRequest) { + return sajuService.getMatchResult(sajuRequest); + } +} diff --git a/src/main/java/project/backend/pythonapi/SajuRequest.java b/src/main/java/project/backend/pythonapi/SajuRequest.java index 6401282..8c94bb1 100644 --- a/src/main/java/project/backend/pythonapi/SajuRequest.java +++ b/src/main/java/project/backend/pythonapi/SajuRequest.java @@ -1,10 +1,10 @@ package project.backend.pythonapi; -import lombok.Builder; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@Builder +@AllArgsConstructor public class SajuRequest { private PersonInfo person1; diff --git a/src/main/java/project/backend/user/UserRepository.java b/src/main/java/project/backend/user/UserRepository.java index 40c868d..920efcc 100644 --- a/src/main/java/project/backend/user/UserRepository.java +++ b/src/main/java/project/backend/user/UserRepository.java @@ -1,5 +1,6 @@ package project.backend.user; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,6 +8,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import project.backend.user.dto.UserEnums; import project.backend.user.entity.User; @Repository @@ -14,4 +16,26 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u JOIN FETCH u.userProfile WHERE u.id = :id") Optional findByIdWithProfile(@Param("id") Long id); + + @Query("SELECT u FROM User u JOIN FETCH u.userProfile " + + "WHERE u.userProfile.region = :region " + + "AND u.userProfile.sexualOrientation = :sexualOrientation " + + "AND u.id != :excludeUserId") + List findMatchingUsersByRegionAndOrientation( + @Param("region") UserEnums.region region, + @Param("sexualOrientation") UserEnums.SexualOrientation sexualOrientation, + @Param("excludeUserId") Long excludeUserId + ); + + @Query("SELECT u FROM User u JOIN FETCH u.userProfile " + + "WHERE u.userProfile.region = :region " + + "AND u.userProfile.sexualOrientation = :sexualOrientation " + + "AND u.id != :excludeUserId " + + "AND u.gender != :myGender") + List findMatchingUsersForStraight( + @Param("region") UserEnums.region region, + @Param("sexualOrientation") UserEnums.SexualOrientation sexualOrientation, + @Param("excludeUserId") Long excludeUserId, + @Param("myGender") UserEnums.Gender myGender + ); } \ No newline at end of file diff --git a/src/main/java/project/backend/user/UserService.java b/src/main/java/project/backend/user/UserService.java index 9d703c0..25f1a75 100644 --- a/src/main/java/project/backend/user/UserService.java +++ b/src/main/java/project/backend/user/UserService.java @@ -34,7 +34,7 @@ public class UserService { @Transactional public UserResponseDTO registerNewUser(SignUpRequestDTO requestDTO, MultipartFile profileImage) throws IOException { UserProfile userProfile = UserProfile.builder() - .sexualOrientation(requestDTO.getSexualorientation()) + .sexualOrientation(requestDTO.getSexualOrientation()) .job(requestDTO.getJob()) .region(requestDTO.getRegion()) .drinkingFrequency(requestDTO.getDrinkingFrequency()) diff --git a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java index b2b9a5a..5441aa8 100644 --- a/src/main/java/project/backend/user/dto/SignUpRequestDTO.java +++ b/src/main/java/project/backend/user/dto/SignUpRequestDTO.java @@ -15,7 +15,7 @@ public class SignUpRequestDTO { private UserEnums.Gender gender; // 상세정보 - private UserEnums.SexualOrientation sexualorientation; + private UserEnums.SexualOrientation sexualOrientation; private UserEnums.Job job; private UserEnums.region region; private UserEnums.DrinkingFrequency drinkingFrequency; diff --git a/src/main/java/project/backend/user/dto/UserProfileDTO.java b/src/main/java/project/backend/user/dto/UserProfileDTO.java index 3f6d381..7dc88e7 100644 --- a/src/main/java/project/backend/user/dto/UserProfileDTO.java +++ b/src/main/java/project/backend/user/dto/UserProfileDTO.java @@ -5,7 +5,7 @@ @Getter public class UserProfileDTO { - private UserEnums.SexualOrientation sexualorientation; + private UserEnums.SexualOrientation sexualOrientation; private UserEnums.Job job; private UserEnums.region region; private UserEnums.DrinkingFrequency drinkingFrequency; diff --git a/src/main/java/project/backend/user/entity/UserProfile.java b/src/main/java/project/backend/user/entity/UserProfile.java index 0da3f1d..9cc2da0 100644 --- a/src/main/java/project/backend/user/entity/UserProfile.java +++ b/src/main/java/project/backend/user/entity/UserProfile.java @@ -58,8 +58,8 @@ public class UserProfile { private String introduction; public void updateUserProfile(UserProfileDTO userProfileDTO) { - if (userProfileDTO.getSexualorientation() != null) { - this.sexualOrientation = userProfileDTO.getSexualorientation(); + if (userProfileDTO.getSexualOrientation() != null) { + this.sexualOrientation = userProfileDTO.getSexualOrientation(); } if (userProfileDTO.getJob() != null) { this.job = userProfileDTO.getJob(); From ce06862252eb65087f14bb860f93558b5f642eda Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 9 Nov 2025 00:39:23 +0900 Subject: [PATCH 21/32] =?UTF-8?q?feat=20:=20spring=20ai=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -spring ai + open ai 연결 및 테스트 완료 --- .../backend/openai/OpenAiController.java | 20 ++++ .../project/backend/openai/OpenAiService.java | 94 +++++++++++++++++++ .../project/backend/openai/PromptRequest.java | 4 + 3 files changed, 118 insertions(+) create mode 100644 src/main/java/project/backend/openai/OpenAiController.java create mode 100644 src/main/java/project/backend/openai/OpenAiService.java create mode 100644 src/main/java/project/backend/openai/PromptRequest.java diff --git a/src/main/java/project/backend/openai/OpenAiController.java b/src/main/java/project/backend/openai/OpenAiController.java new file mode 100644 index 0000000..01ab1c7 --- /dev/null +++ b/src/main/java/project/backend/openai/OpenAiController.java @@ -0,0 +1,20 @@ +package project.backend.openai; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/prompt") +public class OpenAiController { + + private final OpenAiService openAiService; + + @PostMapping + public String getGptResponse(@RequestBody PromptRequest request) { + return openAiService.getGptResponse(request.prompt()); + } +} diff --git a/src/main/java/project/backend/openai/OpenAiService.java b/src/main/java/project/backend/openai/OpenAiService.java new file mode 100644 index 0000000..cc6f97c --- /dev/null +++ b/src/main/java/project/backend/openai/OpenAiService.java @@ -0,0 +1,94 @@ +package project.backend.openai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Service; +import project.backend.fortune.dto.FortuneDTO; + +@Service +public class OpenAiService { + + private final ChatClient chatClient; + private final ObjectMapper objectMapper; + + public OpenAiService(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) { + this.chatClient = chatClientBuilder.build(); + this.objectMapper = objectMapper; + } + + public String getGptResponse(String prompt) { + + return chatClient.prompt() + .user(prompt) + .call() + .content(); + } + + //오늘의 운세(4가지) + public FortuneDTO getTodayFortune() { + String prompt = """ + 오늘의 운세를 다음 JSON 형식으로 반환해주세요. + 각 운세는 한 문단으로 작성해주세요 (100자 이내). + + { + "overallFortune": "총운 설명", + "loveFortune": "애정운 설명", + "moneyFortune": "금전운 설명", + "careerFortune": "직장운 설명" + } + + 반환 형식은 반드시 JSON만 반환하고, 다른 텍스트는 포함하지 마세요. + """; + + try { + String response = chatClient.prompt() + .user(prompt) + .call() + .content(); + + String jsonResponse = extractJsonFromResponse(response); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + return FortuneDTO.builder() + .overallFortune(jsonNode.get("overallFortune").asText()) + .loveFortune(jsonNode.get("loveFortune").asText()) + .moneyFortune(jsonNode.get("moneyFortune").asText()) + .careerFortune(jsonNode.get("careerFortune").asText()) + .build(); + + } catch (Exception e) { + throw new RuntimeException("운세를 가져오는 중 오류가 발생했습니다: " + e.getMessage(), e); + } + } + + //ai 응답에서 json 만 추출 + private String extractJsonFromResponse(String response) { + String trimmed = response.trim(); + + if (trimmed.startsWith("```json")) { + int start = trimmed.indexOf("{"); + int end = trimmed.lastIndexOf("}") + 1; + return trimmed.substring(start, end); + } + + if (trimmed.startsWith("```")) { + int start = trimmed.indexOf("{"); + int end = trimmed.lastIndexOf("}") + 1; + return trimmed.substring(start, end); + } + + if (trimmed.startsWith("{")) { + int end = trimmed.lastIndexOf("}") + 1; + return trimmed.substring(0, end); + } + + int startIndex = trimmed.indexOf("{"); + int endIndex = trimmed.lastIndexOf("}"); + if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) { + return trimmed.substring(startIndex, endIndex + 1); + } + + return trimmed; + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/openai/PromptRequest.java b/src/main/java/project/backend/openai/PromptRequest.java new file mode 100644 index 0000000..791fad3 --- /dev/null +++ b/src/main/java/project/backend/openai/PromptRequest.java @@ -0,0 +1,4 @@ +package project.backend.openai; + +public record PromptRequest(String prompt) { +} \ No newline at end of file From d9efac3e0e5bb9bdfd582869778376da06b71ab2 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 9 Nov 2025 00:40:11 +0900 Subject: [PATCH 22/32] =?UTF-8?q?feat=20:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EC=9A=B4=EC=84=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -총운,애정운,금전운,직장운을 json 형식으로 반환(100자 이하) -openai 사용 --- .../backend/fortune/FortuneController.java | 23 +++++++++++++++++++ .../backend/fortune/FortuneService.java | 18 +++++++++++++++ .../backend/fortune/dto/FortuneDTO.java | 15 ++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/main/java/project/backend/fortune/FortuneController.java create mode 100644 src/main/java/project/backend/fortune/FortuneService.java create mode 100644 src/main/java/project/backend/fortune/dto/FortuneDTO.java diff --git a/src/main/java/project/backend/fortune/FortuneController.java b/src/main/java/project/backend/fortune/FortuneController.java new file mode 100644 index 0000000..abdb43b --- /dev/null +++ b/src/main/java/project/backend/fortune/FortuneController.java @@ -0,0 +1,23 @@ +package project.backend.fortune; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import project.backend.fortune.dto.FortuneDTO; + +@RestController +@RequestMapping("/fortune") +@RequiredArgsConstructor +public class FortuneController { + + private final FortuneService fortuneService; + + @GetMapping + public ResponseEntity getTodayFortune() { + FortuneDTO fortune = fortuneService.getTodayFortune(); + return ResponseEntity.ok(fortune); + } +} + diff --git a/src/main/java/project/backend/fortune/FortuneService.java b/src/main/java/project/backend/fortune/FortuneService.java new file mode 100644 index 0000000..4d108d8 --- /dev/null +++ b/src/main/java/project/backend/fortune/FortuneService.java @@ -0,0 +1,18 @@ +package project.backend.fortune; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import project.backend.fortune.dto.FortuneDTO; +import project.backend.openai.OpenAiService; + +@Service +@RequiredArgsConstructor +public class FortuneService { + + private final OpenAiService openAiService; + + public FortuneDTO getTodayFortune() { + return openAiService.getTodayFortune(); + } +} + diff --git a/src/main/java/project/backend/fortune/dto/FortuneDTO.java b/src/main/java/project/backend/fortune/dto/FortuneDTO.java new file mode 100644 index 0000000..873a483 --- /dev/null +++ b/src/main/java/project/backend/fortune/dto/FortuneDTO.java @@ -0,0 +1,15 @@ +package project.backend.fortune.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FortuneDTO { + + private final String overallFortune; // 총운 + private final String loveFortune; // 애정운 + private final String moneyFortune; // 금전운 + private final String careerFortune; // 직장운 +} + From 65100b947bcd88b63c14a7b4228175efdb20a07e Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 9 Nov 2025 00:43:20 +0900 Subject: [PATCH 23/32] =?UTF-8?q?refactor=20:=20dto=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/backend/matching/MatchingController.java | 3 ++- .../java/project/backend/matching/MatchingService.java | 7 ++++--- .../backend/matching/{ => dto}/MatchingResultDTO.java | 4 ++-- src/main/java/project/backend/openai/OpenAiController.java | 1 + .../project/backend/openai/{ => dto}/PromptRequest.java | 2 +- .../java/project/backend/pythonapi/SajuController.java | 2 ++ src/main/java/project/backend/pythonapi/SajuService.java | 2 ++ .../project/backend/pythonapi/{ => dto}/PersonInfo.java | 2 +- .../project/backend/pythonapi/{ => dto}/SajuRequest.java | 2 +- .../project/backend/pythonapi/{ => dto}/SajuResponse.java | 2 +- 10 files changed, 17 insertions(+), 10 deletions(-) rename src/main/java/project/backend/matching/{ => dto}/MatchingResultDTO.java (73%) rename src/main/java/project/backend/openai/{ => dto}/PromptRequest.java (56%) rename src/main/java/project/backend/pythonapi/{ => dto}/PersonInfo.java (64%) rename src/main/java/project/backend/pythonapi/{ => dto}/SajuRequest.java (82%) rename src/main/java/project/backend/pythonapi/{ => dto}/SajuResponse.java (93%) diff --git a/src/main/java/project/backend/matching/MatchingController.java b/src/main/java/project/backend/matching/MatchingController.java index 404ff51..b4cc41b 100644 --- a/src/main/java/project/backend/matching/MatchingController.java +++ b/src/main/java/project/backend/matching/MatchingController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import project.backend.matching.dto.MatchingResultDTO; @RestController @RequestMapping("/matching") @@ -11,7 +12,7 @@ public class MatchingController { private final MatchingService matchingService; @GetMapping("/{userId}") - public MatchingResultDTO getMatchingResult(@PathVariable("userId") Long userId) throws Exception { + public MatchingResultDTO getMatchingResult(@PathVariable("userId") Long userId) throws Exception { return matchingService.getMatchingResult(userId); } } diff --git a/src/main/java/project/backend/matching/MatchingService.java b/src/main/java/project/backend/matching/MatchingService.java index ce8934e..8dab7dc 100644 --- a/src/main/java/project/backend/matching/MatchingService.java +++ b/src/main/java/project/backend/matching/MatchingService.java @@ -3,11 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import project.backend.matching.dto.MatchingResultDTO; import project.backend.mypage.dto.MyPageDisplayDTO; import project.backend.openai.OpenAiService; -import project.backend.pythonapi.PersonInfo; -import project.backend.pythonapi.SajuRequest; -import project.backend.pythonapi.SajuResponse; +import project.backend.pythonapi.dto.PersonInfo; +import project.backend.pythonapi.dto.SajuRequest; +import project.backend.pythonapi.dto.SajuResponse; import project.backend.pythonapi.SajuService; import project.backend.user.UserRepository; import project.backend.user.dto.UserEnums; diff --git a/src/main/java/project/backend/matching/MatchingResultDTO.java b/src/main/java/project/backend/matching/dto/MatchingResultDTO.java similarity index 73% rename from src/main/java/project/backend/matching/MatchingResultDTO.java rename to src/main/java/project/backend/matching/dto/MatchingResultDTO.java index a4c3746..30a2ee6 100644 --- a/src/main/java/project/backend/matching/MatchingResultDTO.java +++ b/src/main/java/project/backend/matching/dto/MatchingResultDTO.java @@ -1,9 +1,9 @@ -package project.backend.matching; +package project.backend.matching.dto; import lombok.Builder; import lombok.Getter; import project.backend.mypage.dto.MyPageDisplayDTO; -import project.backend.pythonapi.SajuResponse; +import project.backend.pythonapi.dto.SajuResponse; @Getter @Builder diff --git a/src/main/java/project/backend/openai/OpenAiController.java b/src/main/java/project/backend/openai/OpenAiController.java index 01ab1c7..ab89fce 100644 --- a/src/main/java/project/backend/openai/OpenAiController.java +++ b/src/main/java/project/backend/openai/OpenAiController.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import project.backend.openai.dto.PromptRequest; @RestController @RequiredArgsConstructor diff --git a/src/main/java/project/backend/openai/PromptRequest.java b/src/main/java/project/backend/openai/dto/PromptRequest.java similarity index 56% rename from src/main/java/project/backend/openai/PromptRequest.java rename to src/main/java/project/backend/openai/dto/PromptRequest.java index 791fad3..eed60fa 100644 --- a/src/main/java/project/backend/openai/PromptRequest.java +++ b/src/main/java/project/backend/openai/dto/PromptRequest.java @@ -1,4 +1,4 @@ -package project.backend.openai; +package project.backend.openai.dto; public record PromptRequest(String prompt) { } \ No newline at end of file diff --git a/src/main/java/project/backend/pythonapi/SajuController.java b/src/main/java/project/backend/pythonapi/SajuController.java index 1cfd440..62edf07 100644 --- a/src/main/java/project/backend/pythonapi/SajuController.java +++ b/src/main/java/project/backend/pythonapi/SajuController.java @@ -5,6 +5,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import project.backend.pythonapi.dto.SajuRequest; +import project.backend.pythonapi.dto.SajuResponse; import reactor.core.publisher.Mono; @RestController diff --git a/src/main/java/project/backend/pythonapi/SajuService.java b/src/main/java/project/backend/pythonapi/SajuService.java index d7e9e1b..7729fbe 100644 --- a/src/main/java/project/backend/pythonapi/SajuService.java +++ b/src/main/java/project/backend/pythonapi/SajuService.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import project.backend.pythonapi.dto.SajuRequest; +import project.backend.pythonapi.dto.SajuResponse; import reactor.core.publisher.Mono; @Service diff --git a/src/main/java/project/backend/pythonapi/PersonInfo.java b/src/main/java/project/backend/pythonapi/dto/PersonInfo.java similarity index 64% rename from src/main/java/project/backend/pythonapi/PersonInfo.java rename to src/main/java/project/backend/pythonapi/dto/PersonInfo.java index 8a5f7d5..0fb388d 100644 --- a/src/main/java/project/backend/pythonapi/PersonInfo.java +++ b/src/main/java/project/backend/pythonapi/dto/PersonInfo.java @@ -1,4 +1,4 @@ -package project.backend.pythonapi; +package project.backend.pythonapi.dto; public record PersonInfo(int year, int month, int day, int gender) { diff --git a/src/main/java/project/backend/pythonapi/SajuRequest.java b/src/main/java/project/backend/pythonapi/dto/SajuRequest.java similarity index 82% rename from src/main/java/project/backend/pythonapi/SajuRequest.java rename to src/main/java/project/backend/pythonapi/dto/SajuRequest.java index 8c94bb1..a14695c 100644 --- a/src/main/java/project/backend/pythonapi/SajuRequest.java +++ b/src/main/java/project/backend/pythonapi/dto/SajuRequest.java @@ -1,4 +1,4 @@ -package project.backend.pythonapi; +package project.backend.pythonapi.dto; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/project/backend/pythonapi/SajuResponse.java b/src/main/java/project/backend/pythonapi/dto/SajuResponse.java similarity index 93% rename from src/main/java/project/backend/pythonapi/SajuResponse.java rename to src/main/java/project/backend/pythonapi/dto/SajuResponse.java index 381c6e9..fd5080b 100644 --- a/src/main/java/project/backend/pythonapi/SajuResponse.java +++ b/src/main/java/project/backend/pythonapi/dto/SajuResponse.java @@ -1,4 +1,4 @@ -package project.backend.pythonapi; +package project.backend.pythonapi.dto; import com.fasterxml.jackson.annotation.JsonProperty; From 845a0747935536c947ce0ba3738f5c7a5f8b3152 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Mon, 10 Nov 2025 00:13:39 +0900 Subject: [PATCH 24/32] =?UTF-8?q?feat=20:=20=EB=8C=80=ED=99=94=20=EC=A3=BC?= =?UTF-8?q?=EC=A0=9C=20=EC=B6=94=EC=B2=9C=20+=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EC=8A=A4=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -두 기능 다 반환값에 이모지 넣음(와이어프레임과 유사) -챗봇 기능은 구현 x -spring ai 사용 -응답값 아직 저장 x --- .../backend/openai/OpenAiController.java | 82 ++++++++- .../project/backend/openai/OpenAiService.java | 158 ++++++++++++++++++ .../openai/dto/ConversationTopicDTO.java | 14 ++ .../backend/openai/dto/DatingCourseDTO.java | 14 ++ .../openai/dto/RecommendationRequest.java | 8 + 5 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 src/main/java/project/backend/openai/dto/ConversationTopicDTO.java create mode 100644 src/main/java/project/backend/openai/dto/DatingCourseDTO.java create mode 100644 src/main/java/project/backend/openai/dto/RecommendationRequest.java diff --git a/src/main/java/project/backend/openai/OpenAiController.java b/src/main/java/project/backend/openai/OpenAiController.java index ab89fce..09a639a 100644 --- a/src/main/java/project/backend/openai/OpenAiController.java +++ b/src/main/java/project/backend/openai/OpenAiController.java @@ -1,21 +1,93 @@ package project.backend.openai; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import project.backend.openai.dto.ConversationTopicDTO; +import project.backend.openai.dto.DatingCourseDTO; import project.backend.openai.dto.PromptRequest; +import project.backend.openai.dto.RecommendationRequest; +import project.backend.user.UserRepository; +import project.backend.user.entity.User; + +import java.util.Optional; @RestController @RequiredArgsConstructor -@RequestMapping("/prompt") +@RequestMapping("/ai") public class OpenAiController { private final OpenAiService openAiService; + private final UserRepository userRepository; @PostMapping public String getGptResponse(@RequestBody PromptRequest request) { return openAiService.getGptResponse(request.prompt()); } + + //대화주제 추천 + @PostMapping("/conversation-topics") + public ResponseEntity getConversationTopics( + @RequestBody RecommendationRequest request) throws Exception { + + Long myUserId = request.myUserId(); + Long matchedUserId = request.matchedUserId(); + + Optional myUserOptional = userRepository.findByIdWithProfile(myUserId); + if (myUserOptional.isEmpty()) { + throw new Exception("내 사용자 정보를 찾을 수 없습니다. userId: " + myUserId); + } + + Optional matchedUserOptional = userRepository.findByIdWithProfile(matchedUserId); + if (matchedUserOptional.isEmpty()) { + throw new Exception("매칭된 사용자 정보를 찾을 수 없습니다. userId: " + matchedUserId); + } + + User myUser = myUserOptional.get(); + User matchedUser = matchedUserOptional.get(); + + // UserProfile이 없는 경우 예외 처리 + if (myUser.getUserProfile() == null) { + throw new Exception("내 사용자 프로필 정보가 없습니다. userId: " + myUserId); + } + if (matchedUser.getUserProfile() == null) { + throw new Exception("매칭된 사용자 프로필 정보가 없습니다. userId: " + matchedUserId); + } + + ConversationTopicDTO conversationTopics = openAiService.getConversationTopics(myUser, matchedUser); + return ResponseEntity.ok(conversationTopics); + } + + //데이트 코스 추천 + @PostMapping("/dating-courses") + public ResponseEntity getDatingCourses( + @RequestBody RecommendationRequest request) throws Exception { + + Long myUserId = request.myUserId(); + Long matchedUserId = request.matchedUserId(); + + Optional myUserOptional = userRepository.findByIdWithProfile(myUserId); + if (myUserOptional.isEmpty()) { + throw new Exception("내 사용자 정보를 찾을 수 없습니다. userId: " + myUserId); + } + + Optional matchedUserOptional = userRepository.findByIdWithProfile(matchedUserId); + if (matchedUserOptional.isEmpty()) { + throw new Exception("매칭된 사용자 정보를 찾을 수 없습니다. userId: " + matchedUserId); + } + + User myUser = myUserOptional.get(); + User matchedUser = matchedUserOptional.get(); + + // UserProfile이 없는 경우 예외 처리 + if (myUser.getUserProfile() == null) { + throw new Exception("내 사용자 프로필 정보가 없습니다. userId: " + myUserId); + } + if (matchedUser.getUserProfile() == null) { + throw new Exception("매칭된 사용자 프로필 정보가 없습니다. userId: " + matchedUserId); + } + + DatingCourseDTO datingCourses = openAiService.getDatingCourseRecommendation(myUser, matchedUser); + return ResponseEntity.ok(datingCourses); + } } diff --git a/src/main/java/project/backend/openai/OpenAiService.java b/src/main/java/project/backend/openai/OpenAiService.java index cc6f97c..1d2cf3b 100644 --- a/src/main/java/project/backend/openai/OpenAiService.java +++ b/src/main/java/project/backend/openai/OpenAiService.java @@ -5,6 +5,12 @@ import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; import project.backend.fortune.dto.FortuneDTO; +import project.backend.openai.dto.ConversationTopicDTO; +import project.backend.openai.dto.DatingCourseDTO; +import project.backend.user.entity.User; + +import java.util.ArrayList; +import java.util.List; @Service public class OpenAiService { @@ -62,6 +68,158 @@ public FortuneDTO getTodayFortune() { } } + //대화주제 추천 + public ConversationTopicDTO getConversationTopics(User myUser, User matchedUser) { + String userInfo = buildUserInfoString(myUser); + String matchedUserInfo = buildUserInfoString(matchedUser); + + String prompt = String.format(""" + 다음 두 사람의 정보를 바탕으로 대화주제를 추천해주세요. + + [내 정보] + %s + + [상대방 정보] + %s + + 두 사람의 공통 관심사, 성격, 취미 등을 고려하여 대화하기 좋은 주제 5개를 추천해주세요. + 각 주제는 간단명료하게 한 문장으로 작성해주세요. + 각 주제 앞에 내용과 어울리는 이모지를 하나씩 붙여주세요. + + 다음 JSON 형식으로 반환해주세요: + { + "topics": ["😊 주제1", "💡 주제2", "💬 주제3", "🤔 주제4", "💖 주제5"] + } + + 이모지 예시: 😊 💡 💬 🤔 💖 🎯 🌟 🎨 🎵 🎮 📚 🎬 🍔 🎪 🎭 + 반환 형식은 반드시 JSON만 반환하고, 다른 텍스트는 포함하지 마세요. + """, userInfo, matchedUserInfo); + + try { + String response = chatClient.prompt() + .user(prompt) + .call() + .content(); + + String jsonResponse = extractJsonFromResponse(response); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + List topics = new ArrayList<>(); + JsonNode topicsArray = jsonNode.get("topics"); + if (topicsArray != null && topicsArray.isArray()) { + for (JsonNode topic : topicsArray) { + topics.add(topic.asText()); + } + } + + return ConversationTopicDTO.builder() + .topics(topics) + .build(); + + } catch (Exception e) { + throw new RuntimeException("대화주제를 가져오는 중 오류가 발생했습니다: " + e.getMessage(), e); + } + } + + //데이트 코스 추천 + public DatingCourseDTO getDatingCourseRecommendation(User myUser, User matchedUser) { + String userInfo = buildUserInfoString(myUser); + String matchedUserInfo = buildUserInfoString(matchedUser); + String region = myUser.getUserProfile().getRegion() != null + ? myUser.getUserProfile().getRegion().name() + : "서울"; + + String prompt = String.format(""" + 다음 두 사람의 정보를 바탕으로 데이트 코스를 추천해주세요. + + [내 정보] + %s + + [상대방 정보] + %s + + [지역] + %s + + 두 사람의 성격, 취미, 지역을 고려하여 데이트하기 좋은 코스 5개를 추천해주세요. + 각 코스는 다음 형식으로 작성해주세요: "이모지 장소명 - 활동 설명" + - 각 코스 앞에 내용과 어울리는 이모지를 하나씩 붙여주세요 + - 구체적인 장소명을 반드시 포함해주세요 (예: "한강 공원", "CGV 강남", "이태원 맛집 거리") + - 지역에 맞는 실제 존재하는 장소를 추천해주세요 + - 활동 설명도 함께 작성해주세요 + + 다음 JSON 형식으로 반환해주세요: + { + "courses": ["📍 한강 공원 - 저녁 산책과 야경 감상", "🎬 CGV 강남 - 영화 관람 후 카페 투어", "🍽️ 이태원 맛집 거리 - 다양한 음식 체험", "🎨 삼청동 갤러리 투어 - 예술적인 데이트", "🌳 남산타워 - 야경 감상"] + } + + 이모지 예시: 📍 🎬 🍽️ 🎨 🌳 🎯 🎪 🎭 🏛️ 🎵 🎮 📚 🍔 ☕ 🌸 🏖️ + 반환 형식은 반드시 JSON만 반환하고, 다른 텍스트는 포함하지 마세요. + """, userInfo, matchedUserInfo, region); + + try { + String response = chatClient.prompt() + .user(prompt) + .call() + .content(); + + String jsonResponse = extractJsonFromResponse(response); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + List courses = new ArrayList<>(); + JsonNode coursesArray = jsonNode.get("courses"); + if (coursesArray != null && coursesArray.isArray()) { + for (JsonNode course : coursesArray) { + courses.add(course.asText()); + } + } + + return DatingCourseDTO.builder() + .courses(courses) + .build(); + + } catch (Exception e) { + throw new RuntimeException("데이트 코스를 가져오는 중 오류가 발생했습니다: " + e.getMessage(), e); + } + } + + //사용자 정보를 문자열로 변환하는 헬퍼 메서드 + private String buildUserInfoString(User user) { + StringBuilder sb = new StringBuilder(); + sb.append("이름: ").append(user.getName()).append("\n"); + sb.append("성별: ").append(user.getGender()).append("\n"); + + if (user.getUserProfile() != null) { + var profile = user.getUserProfile(); + if (profile.getJob() != null) { + sb.append("직업: ").append(profile.getJob()).append("\n"); + } + if (profile.getMbti() != null) { + sb.append("MBTI: ").append(profile.getMbti()).append("\n"); + } + if (profile.getRegion() != null) { + sb.append("지역: ").append(profile.getRegion()).append("\n"); + } + if (profile.getPetPreference() != null) { + sb.append("반려동물 선호도: ").append(profile.getPetPreference()).append("\n"); + } + if (profile.getDrinkingFrequency() != null) { + sb.append("음주 빈도: ").append(profile.getDrinkingFrequency()).append("\n"); + } + if (profile.getSmokingStatus() != null) { + sb.append("흡연 여부: ").append(profile.getSmokingStatus()).append("\n"); + } + if (profile.getReligion() != null) { + sb.append("종교: ").append(profile.getReligion()).append("\n"); + } + if (profile.getIntroduction() != null && !profile.getIntroduction().isEmpty()) { + sb.append("자기소개: ").append(profile.getIntroduction()).append("\n"); + } + } + + return sb.toString(); + } + //ai 응답에서 json 만 추출 private String extractJsonFromResponse(String response) { String trimmed = response.trim(); diff --git a/src/main/java/project/backend/openai/dto/ConversationTopicDTO.java b/src/main/java/project/backend/openai/dto/ConversationTopicDTO.java new file mode 100644 index 0000000..29ee9d4 --- /dev/null +++ b/src/main/java/project/backend/openai/dto/ConversationTopicDTO.java @@ -0,0 +1,14 @@ +package project.backend.openai.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class ConversationTopicDTO { + + private final List topics; // 대화주제 리스트 (3-5개) +} + diff --git a/src/main/java/project/backend/openai/dto/DatingCourseDTO.java b/src/main/java/project/backend/openai/dto/DatingCourseDTO.java new file mode 100644 index 0000000..41ac79f --- /dev/null +++ b/src/main/java/project/backend/openai/dto/DatingCourseDTO.java @@ -0,0 +1,14 @@ +package project.backend.openai.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class DatingCourseDTO { + + private final List courses; // 데이트 코스 리스트 (3-5개) +} + diff --git a/src/main/java/project/backend/openai/dto/RecommendationRequest.java b/src/main/java/project/backend/openai/dto/RecommendationRequest.java new file mode 100644 index 0000000..a27a95f --- /dev/null +++ b/src/main/java/project/backend/openai/dto/RecommendationRequest.java @@ -0,0 +1,8 @@ +package project.backend.openai.dto; + +public record RecommendationRequest( + Long myUserId, + Long matchedUserId +) { +} + From 38b23de24a9f126478c33ca4cd1b5c58ff5aa2b9 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 15 Nov 2025 21:39:08 +0900 Subject: [PATCH 25/32] =?UTF-8?q?refactor=20:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/kakaoLogin/JwtAuthenticationFilter.java | 4 ++-- .../backend/kakaoLogin/KakaoOAuthService.java | 8 ++++---- .../backend/kakaoLogin/{User.java => KakaoUser.java} | 8 ++++---- .../backend/kakaoLogin/KakaoUserRepository.java | 12 ++++++++++++ .../project/backend/kakaoLogin/UserRepository.java | 10 ---------- 5 files changed, 22 insertions(+), 20 deletions(-) rename src/main/java/project/backend/kakaoLogin/{User.java => KakaoUser.java} (90%) create mode 100644 src/main/java/project/backend/kakaoLogin/KakaoUserRepository.java delete mode 100644 src/main/java/project/backend/kakaoLogin/UserRepository.java diff --git a/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java b/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java index 68959f0..16c4826 100644 --- a/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java +++ b/src/main/java/project/backend/kakaoLogin/JwtAuthenticationFilter.java @@ -20,7 +20,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; - private final UserRepository userRepository; + private final KakaoUserRepository userRepository; @Override protected void doFilterInternal(HttpServletRequest request, @@ -53,7 +53,7 @@ protected void doFilterInternal(HttpServletRequest request, } // DB에서 사용자 조회 - User user = userRepository.findByEmail(subject) + KakaoUser user = userRepository.findByEmail(subject) .orElseGet(() -> userRepository.findByKakaoId(subject).orElse(null)); if (user == null) { diff --git a/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java b/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java index 2c003b8..4abfe6c 100644 --- a/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java +++ b/src/main/java/project/backend/kakaoLogin/KakaoOAuthService.java @@ -23,7 +23,7 @@ @RequiredArgsConstructor public class KakaoOAuthService { - private final UserRepository userRepository; + private final KakaoUserRepository userRepository; private final RefreshTokenRepository refreshTokenRepository; private final JwtTokenProvider jwtTokenProvider; @@ -85,11 +85,11 @@ public JwtTokenResponse loginWithKakao(String code) { String email = kakaoAccount != null ? (String) kakaoAccount.get("email") : null; // DB 사용자 확인/저장 - Optional existingUser = userRepository.findByKakaoId(kakaoId); + Optional existingUser = userRepository.findByKakaoId(kakaoId); boolean isNewUser = existingUser.isEmpty(); - User user = existingUser.orElseGet(() -> { - User newUser = User.builder() + KakaoUser user = existingUser.orElseGet(() -> { + KakaoUser newUser = KakaoUser.builder() .kakaoId(kakaoId) .email(email) .role(Role.USER) diff --git a/src/main/java/project/backend/kakaoLogin/User.java b/src/main/java/project/backend/kakaoLogin/KakaoUser.java similarity index 90% rename from src/main/java/project/backend/kakaoLogin/User.java rename to src/main/java/project/backend/kakaoLogin/KakaoUser.java index fd2366c..0c39268 100644 --- a/src/main/java/project/backend/kakaoLogin/User.java +++ b/src/main/java/project/backend/kakaoLogin/KakaoUser.java @@ -14,7 +14,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import project.backend.entity.BasicInfo; +import project.backend.user.entity.User; @Entity @Getter @@ -22,7 +22,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class User { +public class KakaoUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -38,5 +38,5 @@ public class User { private Role role = Role.USER; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) - private BasicInfo basicInfo; -} \ No newline at end of file + private User user; +} diff --git a/src/main/java/project/backend/kakaoLogin/KakaoUserRepository.java b/src/main/java/project/backend/kakaoLogin/KakaoUserRepository.java new file mode 100644 index 0000000..1db877a --- /dev/null +++ b/src/main/java/project/backend/kakaoLogin/KakaoUserRepository.java @@ -0,0 +1,12 @@ +package project.backend.kakaoLogin; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface KakaoUserRepository extends JpaRepository { + Optional findByKakaoId(String kakaoId); + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/project/backend/kakaoLogin/UserRepository.java b/src/main/java/project/backend/kakaoLogin/UserRepository.java deleted file mode 100644 index 209007c..0000000 --- a/src/main/java/project/backend/kakaoLogin/UserRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package project.backend.kakaoLogin; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRepository extends JpaRepository { - Optional findByKakaoId(String kakaoId); - Optional findByEmail(String email); -} \ No newline at end of file From 3aabcd60b0a6e0f5f67911a2627db4fe392da465 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 16 Nov 2025 00:36:12 +0900 Subject: [PATCH 26/32] =?UTF-8?q?feat=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -스웨거 라이브러리 추가 -스웨거 config 추가 -web security config 클래스에 스웨거 설정 추가(스웨거 문서 열람시 카카오 로그인 로직 통과) --- build.gradle | 3 ++ .../project/backend/config/SwaggerConfig.java | 34 +++++++++++++++++++ .../WebSecurityConfig.java | 13 +++++-- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/java/project/backend/config/SwaggerConfig.java rename src/main/java/project/backend/{kakaoLogin => config}/WebSecurityConfig.java (83%) diff --git a/build.gradle b/build.gradle index 31f804f..5a5e49e 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //스웨거 + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + //상렬이거 implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' diff --git a/src/main/java/project/backend/config/SwaggerConfig.java b/src/main/java/project/backend/config/SwaggerConfig.java new file mode 100644 index 0000000..ed0a1ae --- /dev/null +++ b/src/main/java/project/backend/config/SwaggerConfig.java @@ -0,0 +1,34 @@ +package project.backend.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import java.util.Collections; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components().addSecuritySchemes("bearerAuth", securityScheme())) + .security(Collections.singletonList(new SecurityRequirement().addList("bearerAuth"))) + .info(new Info() + .title("Dating App API") + .description("팀프 2조의 API 문서입니다.") + .version("1.0.0")); + } + + private SecurityScheme securityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization"); + } +} diff --git a/src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java b/src/main/java/project/backend/config/WebSecurityConfig.java similarity index 83% rename from src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java rename to src/main/java/project/backend/config/WebSecurityConfig.java index ac9b140..303d7e6 100644 --- a/src/main/java/project/backend/kakaoLogin/WebSecurityConfig.java +++ b/src/main/java/project/backend/config/WebSecurityConfig.java @@ -1,4 +1,4 @@ -package project.backend.kakaoLogin; +package project.backend.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -16,6 +16,14 @@ @EnableMethodSecurity public class WebSecurityConfig { + private static final String[] SWAGGER_URLS = { + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + }; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -27,7 +35,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth // 테스트를 위해 "/api/**"를 추가함, 추후 보안 고려 시 세밀하게 관리하도록 수정하는 게 좋음 .requestMatchers("/auth/**", "/login/**", "/api/**", "/oauth2/**").permitAll() - .anyRequest().authenticated() + .requestMatchers(SWAGGER_URLS).permitAll() + .anyRequest().authenticated() ) // OAuth2 로그인 설정 .oauth2Login(oauth -> oauth From 5fdedd8b5273d2781ea0a90eaa39a8f51b08cf1c Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 16 Nov 2025 00:38:11 +0900 Subject: [PATCH 27/32] =?UTF-8?q?fix=20:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kakaoUser, User 매핑 버그 수정 -webConfig 파일 위치 변경 --- .../java/project/backend/{user => config}/WebConfig.java | 2 +- src/main/java/project/backend/kakaoLogin/KakaoUser.java | 2 +- src/main/java/project/backend/user/entity/User.java | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) rename src/main/java/project/backend/{user => config}/WebConfig.java (95%) diff --git a/src/main/java/project/backend/user/WebConfig.java b/src/main/java/project/backend/config/WebConfig.java similarity index 95% rename from src/main/java/project/backend/user/WebConfig.java rename to src/main/java/project/backend/config/WebConfig.java index c57888f..2d568c1 100644 --- a/src/main/java/project/backend/user/WebConfig.java +++ b/src/main/java/project/backend/config/WebConfig.java @@ -1,4 +1,4 @@ -package project.backend.user; +package project.backend.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/project/backend/kakaoLogin/KakaoUser.java b/src/main/java/project/backend/kakaoLogin/KakaoUser.java index 0c39268..9ac3061 100644 --- a/src/main/java/project/backend/kakaoLogin/KakaoUser.java +++ b/src/main/java/project/backend/kakaoLogin/KakaoUser.java @@ -37,6 +37,6 @@ public class KakaoUser { @Builder.Default private Role role = Role.USER; - @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) + @OneToOne(mappedBy = "kakaoUser", cascade = CascadeType.ALL) private User user; } diff --git a/src/main/java/project/backend/user/entity/User.java b/src/main/java/project/backend/user/entity/User.java index 558efe8..375bdb2 100644 --- a/src/main/java/project/backend/user/entity/User.java +++ b/src/main/java/project/backend/user/entity/User.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import lombok.*; +import project.backend.kakaoLogin.KakaoUser; import project.backend.user.dto.UserEnums; @Entity @@ -26,6 +27,10 @@ public class User { @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) private UserProfile userProfile; + @OneToOne + @JoinColumn(name = "kakao_user_id") + private KakaoUser kakaoUser; + @Builder public User(String name, UserEnums.Gender gender, LocalDate birthDate, UserProfile userProfile) { this.name = name; From 371fb71b82cd667e0165b2d1babbe00763178ead Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sun, 16 Nov 2025 00:40:04 +0900 Subject: [PATCH 28/32] =?UTF-8?q?feat=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=95=A0=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -controller 클래스에 각각 스웨거 설명 애노테이션 추가 --- .../java/project/backend/fortune/FortuneController.java | 2 ++ .../java/project/backend/kakaoLogin/AuthController.java | 2 ++ .../java/project/backend/matching/MatchingController.java | 2 ++ src/main/java/project/backend/mypage/MyPageController.java | 7 +++++++ src/main/java/project/backend/openai/OpenAiController.java | 2 ++ .../java/project/backend/pythonapi/SajuController.java | 2 ++ src/main/java/project/backend/user/UserInfoController.java | 2 ++ 7 files changed, 19 insertions(+) diff --git a/src/main/java/project/backend/fortune/FortuneController.java b/src/main/java/project/backend/fortune/FortuneController.java index abdb43b..3a57c3e 100644 --- a/src/main/java/project/backend/fortune/FortuneController.java +++ b/src/main/java/project/backend/fortune/FortuneController.java @@ -1,5 +1,6 @@ package project.backend.fortune; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,6 +11,7 @@ @RestController @RequestMapping("/fortune") @RequiredArgsConstructor +@Tag(name = "오늘의 운세",description = "오늘의 운세 기능(포춘쿠키)") public class FortuneController { private final FortuneService fortuneService; diff --git a/src/main/java/project/backend/kakaoLogin/AuthController.java b/src/main/java/project/backend/kakaoLogin/AuthController.java index 276dd62..145ea29 100644 --- a/src/main/java/project/backend/kakaoLogin/AuthController.java +++ b/src/main/java/project/backend/kakaoLogin/AuthController.java @@ -1,5 +1,6 @@ package project.backend.kakaoLogin; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,6 +12,7 @@ @RestController @RequestMapping("/auth") @RequiredArgsConstructor +@Tag(name = "auth-controller" ,description = "카카오 로그인") public class AuthController { private final KakaoOAuthService kakaoOAuthService; diff --git a/src/main/java/project/backend/matching/MatchingController.java b/src/main/java/project/backend/matching/MatchingController.java index b4cc41b..6500c46 100644 --- a/src/main/java/project/backend/matching/MatchingController.java +++ b/src/main/java/project/backend/matching/MatchingController.java @@ -1,5 +1,6 @@ package project.backend.matching; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import project.backend.matching.dto.MatchingResultDTO; @@ -7,6 +8,7 @@ @RestController @RequestMapping("/matching") @RequiredArgsConstructor +@Tag(name = "매칭하기" , description = "매칭하기 기능(자신의 id(유저 id)만 넣으면 됨)") public class MatchingController { private final MatchingService matchingService; diff --git a/src/main/java/project/backend/mypage/MyPageController.java b/src/main/java/project/backend/mypage/MyPageController.java index cd65eab..20c1f0b 100644 --- a/src/main/java/project/backend/mypage/MyPageController.java +++ b/src/main/java/project/backend/mypage/MyPageController.java @@ -2,6 +2,8 @@ import java.io.IOException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -13,12 +15,14 @@ @RestController @RequestMapping("/my-page") @RequiredArgsConstructor +@Tag(name = "마이페이지", description = "마이페이지 관련 컨트롤러(정보 조회, 프로필 수정, 프로필 사진 수정, 회원탈퇴") public class MyPageController { private final MyPageService myPageService; // 마이페이지 정보 조회 @GetMapping("/{userId}") + @Operation(summary = "내 모든 정보 조회" , description = "기본,상세 정보 + 프로필 사진까지 받는 메서드") public ResponseEntity getMyPageInfo(@PathVariable("userId") Long userId) { MyPageDisplayDTO myPageInfo = myPageService.getMyPageInfo(userId); @@ -27,6 +31,7 @@ public ResponseEntity getMyPageInfo(@PathVariable("userId") Lo // 마이페이지 프로필 정보 수정 @PatchMapping("/{userId}/profile") + @Operation(summary = "프로필 정보 수정" ,description = "상세 정보 + 자기소개 글만 수정 가능(프로필 사진은 수정 X)") public ResponseEntity updateProfile( @PathVariable("userId") Long userId, @RequestBody UserProfileDTO userProfileDTO) { @@ -37,6 +42,7 @@ public ResponseEntity updateProfile( // 마이페이지 프로필 이미지 수정 @PatchMapping(value = "/{userId}/profile-image", consumes = "multipart/form-data") + @Operation(summary = "프로필 이미지 수정") public ResponseEntity updateProfileImage( @PathVariable("userId") Long userId, @RequestPart("profileImage") MultipartFile profileImage) throws IOException { @@ -47,6 +53,7 @@ public ResponseEntity updateProfileImage( // 회원 탈퇴 @DeleteMapping("/{userId}") + @Operation(summary = "회원 탈퇴") public ResponseEntity deleteUser(@PathVariable("userId") Long userId) { myPageService.deleteUser(userId); diff --git a/src/main/java/project/backend/openai/OpenAiController.java b/src/main/java/project/backend/openai/OpenAiController.java index 09a639a..91c03a9 100644 --- a/src/main/java/project/backend/openai/OpenAiController.java +++ b/src/main/java/project/backend/openai/OpenAiController.java @@ -1,5 +1,6 @@ package project.backend.openai; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,6 +16,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/ai") +@Tag(name = "spring-ai-controller", description = "프런트에서 사용할 필요 X") public class OpenAiController { private final OpenAiService openAiService; diff --git a/src/main/java/project/backend/pythonapi/SajuController.java b/src/main/java/project/backend/pythonapi/SajuController.java index 62edf07..f3e0897 100644 --- a/src/main/java/project/backend/pythonapi/SajuController.java +++ b/src/main/java/project/backend/pythonapi/SajuController.java @@ -1,5 +1,6 @@ package project.backend.pythonapi; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -12,6 +13,7 @@ @RestController @RequestMapping("/api/match") @RequiredArgsConstructor +@Tag(name = "사주팔자 궁합점수 받기",description = "프런트에서 사용할 필요 X") public class SajuController { private final SajuService matchService; diff --git a/src/main/java/project/backend/user/UserInfoController.java b/src/main/java/project/backend/user/UserInfoController.java index 22a183e..9ce310c 100644 --- a/src/main/java/project/backend/user/UserInfoController.java +++ b/src/main/java/project/backend/user/UserInfoController.java @@ -2,6 +2,7 @@ import java.io.IOException; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -15,6 +16,7 @@ @RestController @RequestMapping("/users") @RequiredArgsConstructor +@Tag(name = "회원가입", description = "json 유저 정보 + 사진파일 필요") public class UserInfoController { private final UserService userService; From 1673169b388a95bc2bd0392d8ef521c49ae45b69 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Mon, 17 Nov 2025 01:52:27 +0900 Subject: [PATCH 29/32] =?UTF-8?q?feat=20:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -1대1 채팅방 생성 -채팅방 조회 -채팅방 목록 조회 -특정 채팅방 메시지 내역 조회 -채팅방 나가기(채팅방 자체가 삭제 + 상대방에게도 사라짐) --- build.gradle | 2 + .../project/backend/chat/ChatController.java | 29 ++++++ .../backend/chat/ChatMessageService.java | 97 +++++++++++++++++++ .../backend/chat/ChatRoomController.java | 86 ++++++++++++++++ .../project/backend/chat/ChatRoomService.java | 76 +++++++++++++++ .../chat/StompAuthChannelInterceptor.java | 76 +++++++++++++++ .../project/backend/chat/WebSocketConfig.java | 48 +++++++++ .../backend/chat/dto/ChatMessageDTO.java | 30 ++++++ .../project/backend/chat/dto/ChatRoomDTO.java | 34 +++++++ .../chat/dto/CreateChatRoomRequest.java | 11 +++ .../chat/dto/SendChatMessageRequest.java | 11 +++ .../backend/chat/entity/ChatMessage.java | 39 ++++++++ .../project/backend/chat/entity/ChatRoom.java | 56 +++++++++++ .../repository/ChatMessageRepository.java | 12 +++ .../chat/repository/ChatRoomRepository.java | 21 ++++ .../backend/config/WebSecurityConfig.java | 34 +++---- 16 files changed, 645 insertions(+), 17 deletions(-) create mode 100644 src/main/java/project/backend/chat/ChatController.java create mode 100644 src/main/java/project/backend/chat/ChatMessageService.java create mode 100644 src/main/java/project/backend/chat/ChatRoomController.java create mode 100644 src/main/java/project/backend/chat/ChatRoomService.java create mode 100644 src/main/java/project/backend/chat/StompAuthChannelInterceptor.java create mode 100644 src/main/java/project/backend/chat/WebSocketConfig.java create mode 100644 src/main/java/project/backend/chat/dto/ChatMessageDTO.java create mode 100644 src/main/java/project/backend/chat/dto/ChatRoomDTO.java create mode 100644 src/main/java/project/backend/chat/dto/CreateChatRoomRequest.java create mode 100644 src/main/java/project/backend/chat/dto/SendChatMessageRequest.java create mode 100644 src/main/java/project/backend/chat/entity/ChatMessage.java create mode 100644 src/main/java/project/backend/chat/entity/ChatRoom.java create mode 100644 src/main/java/project/backend/chat/repository/ChatMessageRepository.java create mode 100644 src/main/java/project/backend/chat/repository/ChatRoomRepository.java diff --git a/build.gradle b/build.gradle index 5a5e49e..7d88778 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT") implementation 'org.springframework.ai:spring-ai-starter-model-openai' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + } dependencyManagement { diff --git a/src/main/java/project/backend/chat/ChatController.java b/src/main/java/project/backend/chat/ChatController.java new file mode 100644 index 0000000..4a3a1af --- /dev/null +++ b/src/main/java/project/backend/chat/ChatController.java @@ -0,0 +1,29 @@ +package project.backend.chat; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; +import project.backend.chat.dto.SendChatMessageRequest; + +import java.security.Principal; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ChatController { + + private final ChatMessageService chatMessageService; + + @MessageMapping("/chat/message") + public void handleMessage(SendChatMessageRequest messageRequest, Principal principal) { + // StompAuthChannelInterceptor가 'User.id' (String)를 넣어줌 + String senderUserIdStr = principal.getName(); + Long senderUserId = Long.parseLong(senderUserIdStr); + + log.info("Message received from User.id {}: roomId={}, content={}", + senderUserId, messageRequest.getRoomId(), messageRequest.getContent()); + + chatMessageService.sendMessage(senderUserId, messageRequest); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/ChatMessageService.java b/src/main/java/project/backend/chat/ChatMessageService.java new file mode 100644 index 0000000..fdec99d --- /dev/null +++ b/src/main/java/project/backend/chat/ChatMessageService.java @@ -0,0 +1,97 @@ +package project.backend.chat; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import project.backend.chat.dto.ChatMessageDTO; +import project.backend.chat.dto.SendChatMessageRequest; +import project.backend.chat.entity.ChatMessage; +import project.backend.chat.entity.ChatRoom; +import project.backend.chat.repository.ChatMessageRepository; +import project.backend.chat.repository.ChatRoomRepository; +import project.backend.user.UserRepository; // KakaoUserRepository 제거 +import project.backend.user.entity.User; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatMessageService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; // User 리포지토리 사용 + private final SimpMessageSendingOperations messagingTemplate; + + /** + * 메시지 전송 및 저장 + * (이제 KakaoUser 관련 로직이 완전히 제거되었습니다) + */ + @Transactional + public void sendMessage(Long senderUserId, SendChatMessageRequest request) { + + // 1. 발신자(Sender) 조회 (User.id로) + User senderUser = userRepository.findById(senderUserId) + .orElseThrow(() -> new EntityNotFoundException("Sender User not found: " + senderUserId)); + + // 2. 채팅방 조회 + ChatRoom room = chatRoomRepository.findById(request.getRoomId()) + .orElseThrow(() -> new EntityNotFoundException("ChatRoom not found: " + request.getRoomId())); + + // 3. 수신자(Receiver) 조회 + User receiverUser = room.getOtherParticipant(senderUser); + if (receiverUser == null) { + // 이 경우는 채팅방에 참여자가 1명이거나 잘못된 경우 + log.error("Receiver not found in room: {}", request.getRoomId()); + throw new EntityNotFoundException("Receiver not found in room"); + } + + // 4. 메시지 엔티티 생성 및 저장 + ChatMessage message = new ChatMessage(room, senderUser, request.getContent()); + chatMessageRepository.save(message); + + // 5. 채팅방 마지막 메시지 업데이트 (목록 정렬용) + room.setLastMessage(message.getContent(), message.getTimestamp()); + // (트랜잭션 종료 시 자동 저장됨) + + // 6. DTO 변환 + ChatMessageDTO messageDTO = ChatMessageDTO.fromEntity(message); + + // 7. WebSocket으로 메시지 전송 + // StompAuthChannelInterceptor에서 User.id를 String으로 Principal에 저장했으므로, + // .convertAndSendToUser의 첫 번째 인자(user)는 User.id의 String 값이어야 합니다. + + // 수신자에게 전송 + messagingTemplate.convertAndSendToUser( + String.valueOf(receiverUser.getId()), // 수신자의 User.id (String) + "/queue/chat", // 구독 주소 + messageDTO // 전송할 메시지 + ); + + // 발신자에게도 전송 (본인 화면 업데이트용) + messagingTemplate.convertAndSendToUser( + String.valueOf(senderUser.getId()), // 발신자의 User.id (String) + "/queue/chat", + messageDTO + ); + + log.info("Message sent from User {} to User {}", senderUser.getId(), receiverUser.getId()); + } + + /** + * 특정 채팅방의 메시지 내역 조회 + */ + @Transactional(readOnly = true) + public List getChatMessages(Long roomId) { + // TODO: (선택) roomId에 현재 로그인한 유저가 포함되어 있는지 확인하는 인가 로직 추가 + return chatMessageRepository.findByChatRoomIdOrderByTimestampAsc(roomId) + .stream() + .map(ChatMessageDTO::fromEntity) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/ChatRoomController.java b/src/main/java/project/backend/chat/ChatRoomController.java new file mode 100644 index 0000000..fa6e4c8 --- /dev/null +++ b/src/main/java/project/backend/chat/ChatRoomController.java @@ -0,0 +1,86 @@ +package project.backend.chat; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import project.backend.chat.dto.ChatMessageDTO; +import project.backend.chat.dto.ChatRoomDTO; +import project.backend.chat.dto.CreateChatRoomRequest; +import project.backend.kakaoLogin.KakaoUser; + +import java.util.List; + +@RestController +@RequestMapping("/api/chat") +@RequiredArgsConstructor +@Tag(name = "채팅 API (REST)", description = "채팅방 생성, 목록 조회, 메시지 내역 조회") +@SecurityRequirement(name = "bearerAuth") +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + private final ChatMessageService chatMessageService; + + @Operation(summary = "1:1 채팅방 생성 또는 조회") + @PostMapping("/room") + public ResponseEntity createOrGetRoom( + @AuthenticationPrincipal KakaoUser kakaoUser, + @RequestBody CreateChatRoomRequest request) { + + if (kakaoUser.getUser() == null) { + return ResponseEntity.status(403).build(); // 회원가입 미완료 + } + Long currentUserId = kakaoUser.getUser().getId(); + + ChatRoomDTO room = chatRoomService.createOrGetRoom(currentUserId, request.getMatchedUserId()); + return ResponseEntity.ok(room); + } + + @Operation(summary = "내 채팅방 목록 조회") + @GetMapping("/rooms") + public ResponseEntity> getMyChatRooms(@AuthenticationPrincipal KakaoUser kakaoUser) { + + Long currentUserId = kakaoUser.getUser().getId(); + List rooms = chatRoomService.getUserChatRooms(currentUserId); + return ResponseEntity.ok(rooms); + } + + @Operation(summary = "특정 채팅방 메시지 내역 조회") + @GetMapping("/room/{roomId}/messages") + public ResponseEntity> getChatMessages( + @PathVariable Long roomId, + @AuthenticationPrincipal KakaoUser kakaoUser) { + + // TODO: (선택) kakaoUser.getUser().getId()가 이 roomId에 접근 권한이 있는지 확인 + + List messages = chatMessageService.getChatMessages(roomId); + return ResponseEntity.ok(messages); + } + + @Operation(summary = "채팅방 나가기 (채팅방 및 대화 내역 삭제)", + description = "채팅방을 나갑니다. 1:1 채팅이므로 방 자체가 삭제되며, 상대방에게도 목록에서 사라집니다.") + @DeleteMapping("/room/{roomId}") + public ResponseEntity leaveChatRoom( + @AuthenticationPrincipal KakaoUser kakaoUser, + @PathVariable Long roomId) { + + if (kakaoUser.getUser() == null) { + return ResponseEntity.status(403).build(); // Forbidden + } + Long currentUserId = kakaoUser.getUser().getId(); + + try { + chatRoomService.deleteChatRoom(currentUserId, roomId); + return ResponseEntity.ok().build(); // 성공 (200 OK) + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); // 방이 없음 (404 Not Found) + } catch (AccessDeniedException e) { + return ResponseEntity.status(403).build(); // 권한 없음 (403 Forbidden) + } + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/ChatRoomService.java b/src/main/java/project/backend/chat/ChatRoomService.java new file mode 100644 index 0000000..a135254 --- /dev/null +++ b/src/main/java/project/backend/chat/ChatRoomService.java @@ -0,0 +1,76 @@ +package project.backend.chat; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import project.backend.chat.dto.ChatRoomDTO; +import project.backend.chat.entity.ChatRoom; +import project.backend.chat.repository.ChatRoomRepository; +import project.backend.user.UserRepository; +import project.backend.user.entity.User; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + private final UserRepository userRepository; + + /** + * 1:1 채팅방 생성 또는 조회 + */ + @Transactional + public ChatRoomDTO createOrGetRoom(Long currentUserId, Long matchedUserId) { + User user1 = userRepository.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("Current user not found: " + currentUserId)); + User user2 = userRepository.findById(matchedUserId) + .orElseThrow(() -> new EntityNotFoundException("Matched user not found: " + matchedUserId)); + + ChatRoom room = chatRoomRepository.findByParticipants(user1, user2) + .orElseGet(() -> { + ChatRoom newRoom = new ChatRoom(user1, user2); + return chatRoomRepository.save(newRoom); + }); + + return ChatRoomDTO.fromEntity(room, user1); + } + + /** + * 내 채팅방 목록 조회 + */ + public List getUserChatRooms(Long currentUserId) { + User user = userRepository.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("Current user not found: " + currentUserId)); + + List rooms = chatRoomRepository.findAllByUser(user); + + return rooms.stream() + .map(room -> ChatRoomDTO.fromEntity(room, user)) + .collect(Collectors.toList()); + } + + /** + * 채팅방 나가기 (채팅방 및 메시지 삭제) + */ + @Transactional + public void deleteChatRoom(Long currentUserId, Long roomId) { + User currentUser = userRepository.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("Current user not found: " + currentUserId)); + + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new EntityNotFoundException("ChatRoom not found: " + roomId)); + + // 요청한 유저가 해당 채팅방의 참여자인지 확인 (인가) + if (!room.getParticipants().contains(currentUser)) { + throw new AccessDeniedException("User is not a participant of this chat room."); + } + + chatRoomRepository.delete(room); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/StompAuthChannelInterceptor.java b/src/main/java/project/backend/chat/StompAuthChannelInterceptor.java new file mode 100644 index 0000000..765bc2a --- /dev/null +++ b/src/main/java/project/backend/chat/StompAuthChannelInterceptor.java @@ -0,0 +1,76 @@ +package project.backend.chat; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Component; +import project.backend.kakaoLogin.JwtTokenProvider; +import project.backend.kakaoLogin.KakaoUser; +import project.backend.kakaoLogin.KakaoUserRepository; +import project.backend.user.entity.User; + +@Slf4j +@Component +@RequiredArgsConstructor +public class StompAuthChannelInterceptor implements ChannelInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + private final KakaoUserRepository kakaoUserRepository; // User.id를 찾기 위해 필요 + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String authToken = accessor.getFirstNativeHeader("Authorization"); + + if (authToken != null && authToken.startsWith("Bearer ")) { + String token = authToken.substring(7); + + if (jwtTokenProvider.validateToken(token)) { + // 1. JWT에서 'kakaoId' 또는 'email' (String) 추출 + String principalIdentifier = jwtTokenProvider.getUserIdFromToken(token); + + if (principalIdentifier != null) { + try { + // 2. DB에서 KakaoUser 조회 + KakaoUser kakaoUser = kakaoUserRepository.findByEmail(principalIdentifier) + .or(() -> kakaoUserRepository.findByKakaoId(principalIdentifier)) + .orElseThrow(() -> new EntityNotFoundException("KakaoUser not found: " + principalIdentifier)); + + // 3. KakaoUser에 연결된 'User' 엔티티 조회 + User user = kakaoUser.getUser(); + if (user == null) { + throw new EntityNotFoundException("User entity not linked for KakaoUser: " + principalIdentifier + ". (Signup not completed)"); + } + + // 4. 'User.id' (Long)를 String으로 변환 + String userId = String.valueOf(user.getId()); + + // 5. WebSocket 세션의 Principal로 'User.id' (String)를 설정 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userId, null, null); + accessor.setUser(authentication); + log.info("STOMP user authenticated. Principal name set to User.id: {}", userId); + + } catch (EntityNotFoundException e) { + log.warn("STOMP connection failed: {}", e.getMessage()); + } + } + } else { + log.warn("STOMP connection failed: Invalid JWT token"); + } + } else { + log.warn("STOMP connection failed: Missing or invalid Authorization header"); + } + } + return message; + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/WebSocketConfig.java b/src/main/java/project/backend/chat/WebSocketConfig.java new file mode 100644 index 0000000..aa8b638 --- /dev/null +++ b/src/main/java/project/backend/chat/WebSocketConfig.java @@ -0,0 +1,48 @@ +package project.backend.chat; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompAuthChannelInterceptor stompAuthChannelInterceptor; + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 1. 메시지 브로커가 처리할 prefix 설정 + // "/queue" -> 1:1 메시징 + // "/topic" -> 브로드캐스팅 (공지 등) + registry.enableSimpleBroker("/queue", "/topic"); + + // 2. 클라이언트가 서버로 메시지를 보낼 때 사용할 prefix + // (예: /app/chat/message) + registry.setApplicationDestinationPrefixes("/app"); + + // 3. 1:1 메시징을 위한 prefix + // (컨트롤러에서 @SendToUser 또는 SimpMessagingTemplate.convertAndSendToUser 사용 시) + // 클라이언트는 /user/queue/chat 와 같은 주소를 구독합니다. + registry.setUserDestinationPrefix("/user"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // WebSocket 연결을 위한 엔드포인트 + registry.addEndpoint("/ws/chat") + .setAllowedOriginPatterns("*") // CORS 허용 + .withSockJS(); // SockJS 지원 + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + // STOMP CONNECT 시 JWT 인증을 처리할 인터셉터 등록 + registration.interceptors(stompAuthChannelInterceptor); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/dto/ChatMessageDTO.java b/src/main/java/project/backend/chat/dto/ChatMessageDTO.java new file mode 100644 index 0000000..548ee21 --- /dev/null +++ b/src/main/java/project/backend/chat/dto/ChatMessageDTO.java @@ -0,0 +1,30 @@ +package project.backend.chat.dto; + +import lombok.Builder; +import lombok.Getter; +import project.backend.chat.entity.ChatMessage; + +import java.time.LocalDateTime; + +// 메시지 조회 및 실시간 전송용 DTO +@Getter +@Builder +public class ChatMessageDTO { + private Long messageId; + private Long roomId; + private Long senderId; + private String senderName; + private String content; + private LocalDateTime timestamp; + + public static ChatMessageDTO fromEntity(ChatMessage message) { + return ChatMessageDTO.builder() + .messageId(message.getId()) + .roomId(message.getChatRoom().getId()) + .senderId(message.getSender().getId()) + .senderName(message.getSender().getName()) + .content(message.getContent()) + .timestamp(message.getTimestamp()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/dto/ChatRoomDTO.java b/src/main/java/project/backend/chat/dto/ChatRoomDTO.java new file mode 100644 index 0000000..78bfecf --- /dev/null +++ b/src/main/java/project/backend/chat/dto/ChatRoomDTO.java @@ -0,0 +1,34 @@ +package project.backend.chat.dto; + +import lombok.Builder; +import lombok.Getter; +import project.backend.chat.entity.ChatRoom; +import project.backend.user.entity.User; + +import java.time.LocalDateTime; + +// 채팅방 목록 조회용 DTO +@Getter +@Builder +public class ChatRoomDTO { + private Long roomId; + private Long otherUserId; + private String otherUserName; + private String otherUserProfileImage; + private String lastMessage; + private LocalDateTime lastMessageTimestamp; + + public static ChatRoomDTO fromEntity(ChatRoom room, User currentUser) { + User otherUser = room.getOtherParticipant(currentUser); + String profileImage = (otherUser.getUserProfile() != null) ? otherUser.getUserProfile().getProfileImagePath() : null; + + return ChatRoomDTO.builder() + .roomId(room.getId()) + .otherUserId(otherUser.getId()) + .otherUserName(otherUser.getName()) + .otherUserProfileImage(profileImage) + .lastMessage(room.getLastMessage()) + .lastMessageTimestamp(room.getLastMessageTimestamp()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/dto/CreateChatRoomRequest.java b/src/main/java/project/backend/chat/dto/CreateChatRoomRequest.java new file mode 100644 index 0000000..84e2116 --- /dev/null +++ b/src/main/java/project/backend/chat/dto/CreateChatRoomRequest.java @@ -0,0 +1,11 @@ +package project.backend.chat.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CreateChatRoomRequest { + // 매칭된 상대방의 User ID + private Long matchedUserId; +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/dto/SendChatMessageRequest.java b/src/main/java/project/backend/chat/dto/SendChatMessageRequest.java new file mode 100644 index 0000000..4f1565d --- /dev/null +++ b/src/main/java/project/backend/chat/dto/SendChatMessageRequest.java @@ -0,0 +1,11 @@ +package project.backend.chat.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SendChatMessageRequest { + private Long roomId; + private String content; +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/entity/ChatMessage.java b/src/main/java/project/backend/chat/entity/ChatMessage.java new file mode 100644 index 0000000..ce7e9c3 --- /dev/null +++ b/src/main/java/project/backend/chat/entity/ChatMessage.java @@ -0,0 +1,39 @@ +package project.backend.chat.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.backend.user.entity.User; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +public class ChatMessage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private ChatRoom chatRoom; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id", nullable = false) + private User sender; + + @Column(nullable = false, length = 1000) + private String content; + + @Column(nullable = false) + private LocalDateTime timestamp; + + public ChatMessage(ChatRoom chatRoom, User sender, String content) { + this.chatRoom = chatRoom; + this.sender = sender; + this.content = content; + this.timestamp = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/entity/ChatRoom.java b/src/main/java/project/backend/chat/entity/ChatRoom.java new file mode 100644 index 0000000..cc81429 --- /dev/null +++ b/src/main/java/project/backend/chat/entity/ChatRoom.java @@ -0,0 +1,56 @@ +package project.backend.chat.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.backend.user.entity.User; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@Getter +@NoArgsConstructor +public class ChatRoom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 두 사용자를 저장. ManyToMany를 사용해 유연하게 관리 + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "chatroom_participants", + joinColumns = @JoinColumn(name = "room_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + private Set participants = new HashSet<>(); + + // 가장 마지막 메시지 (채팅방 목록 정렬용) + private String lastMessage; + private LocalDateTime lastMessageTimestamp; + + @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL, orphanRemoval = true) + private List messages; + + public ChatRoom(User user1, User user2) { + this.participants.add(user1); + this.participants.add(user2); + this.lastMessageTimestamp = LocalDateTime.now(); + } + + public void setLastMessage(String lastMessage, LocalDateTime timestamp) { + this.lastMessage = lastMessage; + this.lastMessageTimestamp = timestamp; + } + + // 채팅방에서 상대방 유저 찾기 + public User getOtherParticipant(User currentUser) { + return participants.stream() + .filter(user -> !user.getId().equals(currentUser.getId())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/repository/ChatMessageRepository.java b/src/main/java/project/backend/chat/repository/ChatMessageRepository.java new file mode 100644 index 0000000..2b0ebc3 --- /dev/null +++ b/src/main/java/project/backend/chat/repository/ChatMessageRepository.java @@ -0,0 +1,12 @@ +package project.backend.chat.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import project.backend.chat.entity.ChatMessage; + +import java.util.List; + +public interface ChatMessageRepository extends JpaRepository { + + // 특정 채팅방의 모든 메시지 조회 (시간순) + List findByChatRoomIdOrderByTimestampAsc(Long chatRoomId); +} \ No newline at end of file diff --git a/src/main/java/project/backend/chat/repository/ChatRoomRepository.java b/src/main/java/project/backend/chat/repository/ChatRoomRepository.java new file mode 100644 index 0000000..3458345 --- /dev/null +++ b/src/main/java/project/backend/chat/repository/ChatRoomRepository.java @@ -0,0 +1,21 @@ +package project.backend.chat.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import project.backend.chat.entity.ChatRoom; +import project.backend.user.entity.User; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomRepository extends JpaRepository { + + // 특정 유저가 참여하고 있는 모든 채팅방 목록 조회 (최신 메시지 순 정렬) + @Query("SELECT r FROM ChatRoom r WHERE :user MEMBER OF r.participants ORDER BY r.lastMessageTimestamp DESC") + List findAllByUser(@Param("user") User user); + + // 두 명의 유저로 채팅방 찾기 + @Query("SELECT r FROM ChatRoom r WHERE :user1 MEMBER OF r.participants AND :user2 MEMBER OF r.participants") + Optional findByParticipants(@Param("user1") User user1, @Param("user2") User user2); +} \ No newline at end of file diff --git a/src/main/java/project/backend/config/WebSecurityConfig.java b/src/main/java/project/backend/config/WebSecurityConfig.java index 303d7e6..b92947d 100644 --- a/src/main/java/project/backend/config/WebSecurityConfig.java +++ b/src/main/java/project/backend/config/WebSecurityConfig.java @@ -27,23 +27,23 @@ public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // csrf 비활성화 - .csrf(csrf -> csrf.disable()) - // CORS 설정 허용 - .cors(Customizer.withDefaults()) - // 요청별 인가 규칙 - .authorizeHttpRequests(auth -> auth - // 테스트를 위해 "/api/**"를 추가함, 추후 보안 고려 시 세밀하게 관리하도록 수정하는 게 좋음 - .requestMatchers("/auth/**", "/login/**", "/api/**", "/oauth2/**").permitAll() - .requestMatchers(SWAGGER_URLS).permitAll() - .anyRequest().authenticated() - ) - // OAuth2 로그인 설정 - .oauth2Login(oauth -> oauth - .defaultSuccessUrl("/auth/kakao/callback", true) - ) - // 세션 비활성화 (JWT 사용 시) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + // csrf 비활성화 + .csrf(csrf -> csrf.disable()) + // CORS 설정 허용 + .cors(Customizer.withDefaults()) + // 요청별 인가 규칙 + .authorizeHttpRequests(auth -> auth + // 테스트를 위해 "/api/**"를 추가함, 추후 보안 고려 시 세밀하게 관리하도록 수정하는 게 좋음 + .requestMatchers("/auth/**", "/login/**", "/api/**", "/oauth2/**", "/ws/chat/**").permitAll() + .requestMatchers(SWAGGER_URLS).permitAll() + .anyRequest().authenticated() + ) + // OAuth2 로그인 설정 + .oauth2Login(oauth -> oauth + .defaultSuccessUrl("/auth/kakao/callback", true) + ) + // 세션 비활성화 (JWT 사용 시) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build(); } From ad18925c9ea89975c8955d7e9c94cb1e709541a1 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Sat, 22 Nov 2025 02:12:38 +0900 Subject: [PATCH 30/32] =?UTF-8?q?feat=20:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EC=9A=B4=EC=84=B8=20=EC=BA=90=EC=8B=9C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -오늘의 운세 캐시 저장 + 하루별로 초기화 --- build.gradle | 2 ++ .../project/backend/BackendApplication.java | 2 ++ .../project/backend/config/CacheConfig.java | 21 +++++++++++++++++++ .../backend/fortune/FortuneService.java | 2 ++ 4 files changed, 27 insertions(+) create mode 100644 src/main/java/project/backend/config/CacheConfig.java diff --git a/build.gradle b/build.gradle index 7d88778..1d0a2b5 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,8 @@ dependencies { implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT") implementation 'org.springframework.ai:spring-ai-starter-model-openai' implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.github.ben-manes.caffeine:caffeine' } diff --git a/src/main/java/project/backend/BackendApplication.java b/src/main/java/project/backend/BackendApplication.java index ceb02f5..7019853 100644 --- a/src/main/java/project/backend/BackendApplication.java +++ b/src/main/java/project/backend/BackendApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication +@EnableCaching public class BackendApplication { public static void main(String[] args) { diff --git a/src/main/java/project/backend/config/CacheConfig.java b/src/main/java/project/backend/config/CacheConfig.java new file mode 100644 index 0000000..af36f8a --- /dev/null +++ b/src/main/java/project/backend/config/CacheConfig.java @@ -0,0 +1,21 @@ +package project.backend.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + + cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(100)); + cacheManager.setCacheNames(java.util.Collections.singletonList("fortunes")); + + return cacheManager; + } +} diff --git a/src/main/java/project/backend/fortune/FortuneService.java b/src/main/java/project/backend/fortune/FortuneService.java index 4d108d8..0ff0f1e 100644 --- a/src/main/java/project/backend/fortune/FortuneService.java +++ b/src/main/java/project/backend/fortune/FortuneService.java @@ -1,6 +1,7 @@ package project.backend.fortune; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import project.backend.fortune.dto.FortuneDTO; import project.backend.openai.OpenAiService; @@ -11,6 +12,7 @@ public class FortuneService { private final OpenAiService openAiService; + @Cacheable(value = "fortunes" ,key = "T(java.time.LocalDate).now().toString()") public FortuneDTO getTodayFortune() { return openAiService.getTodayFortune(); } From e37a0c83f05ff3e11bd993f3befbf6d109818721 Mon Sep 17 00:00:00 2001 From: audwns03 Date: Mon, 24 Nov 2025 02:07:40 +0900 Subject: [PATCH 31/32] =?UTF-8?q?feat=20:=20=EA=B6=81=ED=95=A9=EC=A0=90?= =?UTF-8?q?=EC=88=98=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=82=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -상대방과 내 궁합점수 조회 --- .../backend/chat/ChatRoomController.java | 17 +++++++ .../project/backend/chat/ChatRoomService.java | 31 +++++++++++++ .../matching/MatchSajuInfoRepository.java | 16 +++++++ .../backend/matching/MatchingService.java | 16 ++++++- .../matching/entity/MatchSajuInfo.java | 44 +++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/backend/matching/MatchSajuInfoRepository.java create mode 100644 src/main/java/project/backend/matching/entity/MatchSajuInfo.java diff --git a/src/main/java/project/backend/chat/ChatRoomController.java b/src/main/java/project/backend/chat/ChatRoomController.java index fa6e4c8..2651c8c 100644 --- a/src/main/java/project/backend/chat/ChatRoomController.java +++ b/src/main/java/project/backend/chat/ChatRoomController.java @@ -13,6 +13,7 @@ import project.backend.chat.dto.ChatRoomDTO; import project.backend.chat.dto.CreateChatRoomRequest; import project.backend.kakaoLogin.KakaoUser; +import project.backend.pythonapi.dto.SajuResponse; import java.util.List; @@ -83,4 +84,20 @@ public ResponseEntity leaveChatRoom( return ResponseEntity.status(403).build(); // 권한 없음 (403 Forbidden) } } + + @Operation(summary = "채팅방 궁합 점수 조회", description = "채팅방 ID를 통해 저장된 두 사람의 사주 궁합 결과를 조회") + @GetMapping("/room/{roomId}/saju") + public ResponseEntity getSajuInfo( + @PathVariable Long roomId, + @AuthenticationPrincipal KakaoUser kakaoUser) { + + if (kakaoUser.getUser() == null) { + return ResponseEntity.status(403).build(); + } + Long currentUserId = kakaoUser.getUser().getId(); + + SajuResponse response = chatRoomService.getSajuInfoInRoom(roomId, currentUserId); + + return ResponseEntity.ok(response); + } } \ No newline at end of file diff --git a/src/main/java/project/backend/chat/ChatRoomService.java b/src/main/java/project/backend/chat/ChatRoomService.java index a135254..e982bf5 100644 --- a/src/main/java/project/backend/chat/ChatRoomService.java +++ b/src/main/java/project/backend/chat/ChatRoomService.java @@ -8,6 +8,9 @@ import project.backend.chat.dto.ChatRoomDTO; import project.backend.chat.entity.ChatRoom; import project.backend.chat.repository.ChatRoomRepository; +import project.backend.matching.MatchSajuInfoRepository; +import project.backend.matching.entity.MatchSajuInfo; +import project.backend.pythonapi.dto.SajuResponse; import project.backend.user.UserRepository; import project.backend.user.entity.User; @@ -21,6 +24,7 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; + private final MatchSajuInfoRepository matchSajuInfoRepository; /** * 1:1 채팅방 생성 또는 조회 @@ -73,4 +77,31 @@ public void deleteChatRoom(Long currentUserId, Long roomId) { chatRoomRepository.delete(room); } + + //채팅방 내에서 상대방과 내 궁합점수 조회 + public SajuResponse getSajuInfoInRoom(Long roomId, Long currentUserId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new EntityNotFoundException("ChatRoom not found: " + roomId)); + + User me = userRepository.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("Current user not found")); + + User partner = room.getOtherParticipant(me); + if (partner == null) { + throw new EntityNotFoundException("Partner not found in this room"); + } + + MatchSajuInfo info = matchSajuInfoRepository.findByUsers(me, partner) + .orElseThrow(() -> new IllegalArgumentException("두 유저 사이의 궁합 정보가 없습니다.")); + + return new SajuResponse( + info.getOriginalScore(), + info.getFinalScore(), + info.getStressScore(), + info.getPerson1SalAnalysis(), + info.getPerson2SalAnalysis(), + info.getMatchAnalysis(), + null + ); + } } \ No newline at end of file diff --git a/src/main/java/project/backend/matching/MatchSajuInfoRepository.java b/src/main/java/project/backend/matching/MatchSajuInfoRepository.java new file mode 100644 index 0000000..6c36e8f --- /dev/null +++ b/src/main/java/project/backend/matching/MatchSajuInfoRepository.java @@ -0,0 +1,16 @@ +package project.backend.matching; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import project.backend.matching.entity.MatchSajuInfo; +import project.backend.user.entity.User; +import java.util.Optional; + +public interface MatchSajuInfoRepository extends JpaRepository { + + @Query("SELECT m FROM MatchSajuInfo m WHERE " + + "(m.user = :user AND m.matchedUser = :partner) OR " + + "(m.user = :partner AND m.matchedUser = :user)") + Optional findByUsers(@Param("user") User user, @Param("partner") User partner); +} \ No newline at end of file diff --git a/src/main/java/project/backend/matching/MatchingService.java b/src/main/java/project/backend/matching/MatchingService.java index 8dab7dc..ab7979a 100644 --- a/src/main/java/project/backend/matching/MatchingService.java +++ b/src/main/java/project/backend/matching/MatchingService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import project.backend.matching.dto.MatchingResultDTO; +import project.backend.matching.entity.MatchSajuInfo; import project.backend.mypage.dto.MyPageDisplayDTO; import project.backend.openai.OpenAiService; import project.backend.pythonapi.dto.PersonInfo; @@ -26,7 +27,7 @@ public class MatchingService { private final SajuService sajuService; private final UserRepository userRepository; - private final OpenAiService openAiService; + private final MatchSajuInfoRepository matchSajuInfoRepository; //전체 매칭하기 기능 반환 public MatchingResultDTO getMatchingResult(Long userId) throws Exception { @@ -62,6 +63,19 @@ public MatchingResultDTO getMatchingResult(Long userId) throws Exception { Mono sajuResponseMono = getSajuResponse(sajuRequest); SajuResponse sajuResponse = sajuResponseMono.block(); + MatchSajuInfo matchInfo = MatchSajuInfo.builder() + .user(user) + .matchedUser(randomUser) + .originalScore(sajuResponse.originalScore()) + .finalScore(sajuResponse.finalScore()) + .stressScore(sajuResponse.stressScore()) + .person1SalAnalysis(sajuResponse.person1SalAnalysis()) + .person2SalAnalysis(sajuResponse.person2SalAnalysis()) + .matchAnalysis(sajuResponse.matchAnalysis()) + .build(); + + matchSajuInfoRepository.save(matchInfo); + MyPageDisplayDTO myPageDisplayDTO = new MyPageDisplayDTO(randomUser, randomUser.getUserProfile()); return MatchingResultDTO.builder().sajuResponse(sajuResponse).personInfo(myPageDisplayDTO).build(); diff --git a/src/main/java/project/backend/matching/entity/MatchSajuInfo.java b/src/main/java/project/backend/matching/entity/MatchSajuInfo.java new file mode 100644 index 0000000..dc19e55 --- /dev/null +++ b/src/main/java/project/backend/matching/entity/MatchSajuInfo.java @@ -0,0 +1,44 @@ +package project.backend.matching.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.backend.user.entity.User; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "match_saju_info") +public class MatchSajuInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 매칭을 요청한 사람 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + // 매칭된 상대방 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "matched_user_id") + private User matchedUser; + + // SajuResponse 데이터 필드들 + private double originalScore; + + private double finalScore; + + private double stressScore; + + private String person1SalAnalysis; + + private String person2SalAnalysis; + + private String matchAnalysis; +} \ No newline at end of file From 3ab11f4ba26a9eaab89616332ae204fd257bb00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=AA=85=EC=A4=80?= Date: Fri, 28 Nov 2025 15:55:08 +0900 Subject: [PATCH 32/32] =?UTF-8?q?AuthController=20=EC=95=B1=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=88=98=EC=A0=95=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: lsryl13578 --- .../backend/kakaoLogin/AuthController.java | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/project/backend/kakaoLogin/AuthController.java b/src/main/java/project/backend/kakaoLogin/AuthController.java index 145ea29..10fb5ff 100644 --- a/src/main/java/project/backend/kakaoLogin/AuthController.java +++ b/src/main/java/project/backend/kakaoLogin/AuthController.java @@ -1,14 +1,40 @@ package project.backend.kakaoLogin; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +@Tag(name = "auth-controller", description = "카카오 로그인") +public class AuthController { + + private final KakaoOAuthService kakaoOAuthService; + + @GetMapping("/kakao/callback") + public RedirectView kakaoCallback(@RequestParam("code") String code) { + + JwtTokenResponse tokens = kakaoOAuthService.loginWithKakao(code); + + // 딥링크 URL 구성 (앱으로 반환) + String redirectUrl = String.format( + "divineapp://auth/kakao/callback?accessToken=%s&refreshToken=%s&newUser=%s", + tokens.getAccessToken(), + tokens.getRefreshToken(), + tokens.isNewUser() + ); + + return new RedirectView(redirectUrl); + } +} + +/* @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -22,4 +48,5 @@ public ResponseEntity kakaoCallback(@RequestParam("code") Stri JwtTokenResponse tokens = kakaoOAuthService.loginWithKakao(code); return ResponseEntity.ok(tokens); } -} \ No newline at end of file +} +*/ \ No newline at end of file