From 1ce806331fd66a1ff297dd9efb24ec460135735f Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 14:32:49 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20User=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EC=97=90=20JPA=20Auditing=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/back/domain/user/entity/User.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/back/domain/user/entity/User.java b/src/main/java/com/back/domain/user/entity/User.java index d1d64476..3e0c21a0 100644 --- a/src/main/java/com/back/domain/user/entity/User.java +++ b/src/main/java/com/back/domain/user/entity/User.java @@ -4,6 +4,8 @@ import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -33,8 +35,12 @@ public class User { private Double abvDegree; // 알콜도수(회원 등급) + @CreatedDate // JPA Auditing 적용 + @Column(nullable = false, updatable = false) private LocalDateTime createdAt; // 생성 날짜 + @LastModifiedDate // JPA Auditing 적용 + @Column(nullable = false) private LocalDateTime updatedAt; // 수정 날짜 @Builder.Default From 2364cb4945e81136530d9cdbea0f56e2a21bf215 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 15:23:27 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/profile/dto/ProfileResponseDto.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/back/domain/profile/dto/ProfileResponseDto.java diff --git a/src/main/java/com/back/domain/profile/dto/ProfileResponseDto.java b/src/main/java/com/back/domain/profile/dto/ProfileResponseDto.java new file mode 100644 index 00000000..f42277f1 --- /dev/null +++ b/src/main/java/com/back/domain/profile/dto/ProfileResponseDto.java @@ -0,0 +1,18 @@ +package com.back.domain.profile.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ProfileResponseDto { + private Long id; + private String nickname; + private String email; + + // 서버에 저장된 실제 값(0~100) + private Double abvDegree; // 83.2 + // 표현용(서버에서 계산) + private Integer abvLevel; // 1~6 + private String abvLabel; // "83.2%" +} From 7eb0faa964d591f52e4f978f74f547562468364f Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 15:24:21 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20ABV=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8D=BC=EC=84=BC=ED=8A=B8=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=EB=A7=81=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 --- .../com/back/domain/user/support/AbvView.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/com/back/domain/user/support/AbvView.java diff --git a/src/main/java/com/back/domain/user/support/AbvView.java b/src/main/java/com/back/domain/user/support/AbvView.java new file mode 100644 index 00000000..72d26e8f --- /dev/null +++ b/src/main/java/com/back/domain/user/support/AbvView.java @@ -0,0 +1,28 @@ +package com.back.domain.user.support; + +public final class AbvView { + + private AbvView(){} + + // 0~100%를 6단계로 매핑 + public static int levelOf(Double percent) { + if (percent == null) return 1; + + double x = Math.max(0, Math.min(100, percent)); + int p = (int) x; + + if (p <= 10) return 1; // 0~10 + if (p <= 25) return 2; // 11~25 + if (p <= 45) return 3; // 26~45 + if (p <= 65) return 4; // 46~65 + if (p <= 85) return 5; // 66~85 + return 6; // 86~100 + } + + // 화면용 "23.5%" 라벨 + public static String percentLabel(Double percent) { + if (percent == null) return "0%"; + double x = Math.max(0.0, Math.min(100.0, percent)); + return (x % 1.0 == 0.0) ? String.format("%.0f%%", x) : String.format("%.1f%%", x); + } +} From d3b59b1550e73f7b57bb19079f4f93f93b52b28a Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 15:25:52 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=99=95=EC=9D=B8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/back/domain/user/repository/UserRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/back/domain/user/repository/UserRepository.java b/src/main/java/com/back/domain/user/repository/UserRepository.java index ce4bcc8a..040d5ef1 100644 --- a/src/main/java/com/back/domain/user/repository/UserRepository.java +++ b/src/main/java/com/back/domain/user/repository/UserRepository.java @@ -6,5 +6,6 @@ @Repository public interface UserRepository extends JpaRepository { + boolean existsByNicknameAndIdNot(String nickname, Long id); } From 5b9a8778a0e5c97c1194bd35f16328b117993529 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 15:45:22 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/service/ProfileService.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/back/domain/profile/service/ProfileService.java diff --git a/src/main/java/com/back/domain/profile/service/ProfileService.java b/src/main/java/com/back/domain/profile/service/ProfileService.java new file mode 100644 index 00000000..5fe694d2 --- /dev/null +++ b/src/main/java/com/back/domain/profile/service/ProfileService.java @@ -0,0 +1,34 @@ +package com.back.domain.profile.service; + +import com.back.domain.profile.dto.ProfileResponseDto; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.domain.user.support.AbvView; +import com.back.global.exception.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProfileService { + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public ProfileResponseDto getProfile(Long id) { + User user = userRepository.findById(id).orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); + + Double percent = user.getAbvDegree(); + int level = AbvView.levelOf(percent); + String label = AbvView.percentLabel(percent); + + return ProfileResponseDto.builder() + .id(user.getId()) + .nickname(user.getNickname()) + .abvDegree(percent) + .abvLevel(level) + .abvLabel(label) + .build(); + } +} From 835799c5a1160f96b4e0cac1467aea0985cb9d5f Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:07:27 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=84=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/back/domain/user/entity/User.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/back/domain/user/entity/User.java b/src/main/java/com/back/domain/user/entity/User.java index 3e0c21a0..036d95d2 100644 --- a/src/main/java/com/back/domain/user/entity/User.java +++ b/src/main/java/com/back/domain/user/entity/User.java @@ -47,8 +47,6 @@ public class User { @Column(nullable = false, length = 20) private String role = "USER"; - private String profileImgUrl; - public boolean isAdmin() { return "ADMIN".equalsIgnoreCase(role); } From 5abc38f6a64f99133a1905fcf4cba190e4b179a0 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:08:14 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/controller/ProfileController.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/com/back/domain/profile/controller/ProfileController.java diff --git a/src/main/java/com/back/domain/profile/controller/ProfileController.java b/src/main/java/com/back/domain/profile/controller/ProfileController.java new file mode 100644 index 00000000..2bd8e66f --- /dev/null +++ b/src/main/java/com/back/domain/profile/controller/ProfileController.java @@ -0,0 +1,26 @@ +package com.back.domain.profile.controller; + +import com.back.domain.profile.dto.ProfileResponseDto; +import com.back.domain.profile.service.ProfileService; +import com.back.domain.user.service.UserService; +import com.back.global.rsData.RsData; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/me/profile") +@RequiredArgsConstructor +public class ProfileController { + + private final UserService userService; + private final ProfileService profileService; + + @GetMapping + public RsData getProfile(@AuthenticationPrincipal(expression = "id") Long userId) { + ProfileResponseDto body = profileService.getProfile(userId); + return RsData.successOf(body); // code=200, message="success" + } +} From 8d5f4b2d2772f5a6806ca87fdecdb31e2685b6d2 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:16:10 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20DTO=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 --- .../profile/dto/ProfileUpdateRequestDto.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/com/back/domain/profile/dto/ProfileUpdateRequestDto.java diff --git a/src/main/java/com/back/domain/profile/dto/ProfileUpdateRequestDto.java b/src/main/java/com/back/domain/profile/dto/ProfileUpdateRequestDto.java new file mode 100644 index 00000000..1a1cf1aa --- /dev/null +++ b/src/main/java/com/back/domain/profile/dto/ProfileUpdateRequestDto.java @@ -0,0 +1,17 @@ +package com.back.domain.profile.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ProfileUpdateRequestDto { + + @Size(min = 1, max = 10, message = "닉네임은 1~10자") + private String nickname; + + @Email(message = "이메일 형식이 아닙니다") + private String email; +} From a6fd975d535c16163c9450066af5b5e41e599b77 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:21:53 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/service/ProfileService.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/com/back/domain/profile/service/ProfileService.java b/src/main/java/com/back/domain/profile/service/ProfileService.java index 5fe694d2..c48cd155 100644 --- a/src/main/java/com/back/domain/profile/service/ProfileService.java +++ b/src/main/java/com/back/domain/profile/service/ProfileService.java @@ -1,6 +1,7 @@ package com.back.domain.profile.service; import com.back.domain.profile.dto.ProfileResponseDto; +import com.back.domain.profile.dto.ProfileUpdateRequestDto; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; import com.back.domain.user.support.AbvView; @@ -31,4 +32,31 @@ public ProfileResponseDto getProfile(Long id) { .abvLabel(label) .build(); } + + @Transactional + public ProfileResponseDto updateProfile(Long id, ProfileUpdateRequestDto profileUpdateRequestDto) { + User user = userRepository.findById(id).orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); + + if (profileUpdateRequestDto.getNickname() != null) { + String nickname = profileUpdateRequestDto.getNickname().trim(); + if (nickname.isEmpty() || nickname.length() > 10) { + throw new ServiceException(400, "닉네임은 1~10자"); + } + + if (userRepository.existsByNicknameAndIdNot(nickname, id)) { + throw new ServiceException(409, "이미 사용중인 닉네임"); + } + + user.setNickname(nickname); + } + + if (profileUpdateRequestDto.getEmail() != null) { + String email = profileUpdateRequestDto.getEmail().trim(); + user.setEmail(email.isEmpty() ? null : email); + } + + userRepository.save(user); + + return getProfile(id); + } } From 5638bca898262a7114c495ddc4248198d3485f25 Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:24:18 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20API=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 --- .../domain/profile/controller/ProfileController.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/back/domain/profile/controller/ProfileController.java b/src/main/java/com/back/domain/profile/controller/ProfileController.java index 2bd8e66f..c572924e 100644 --- a/src/main/java/com/back/domain/profile/controller/ProfileController.java +++ b/src/main/java/com/back/domain/profile/controller/ProfileController.java @@ -1,14 +1,14 @@ package com.back.domain.profile.controller; import com.back.domain.profile.dto.ProfileResponseDto; +import com.back.domain.profile.dto.ProfileUpdateRequestDto; import com.back.domain.profile.service.ProfileService; import com.back.domain.user.service.UserService; import com.back.global.rsData.RsData; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/me/profile") @@ -23,4 +23,10 @@ public RsData getProfile(@AuthenticationPrincipal(expression ProfileResponseDto body = profileService.getProfile(userId); return RsData.successOf(body); // code=200, message="success" } + + @PutMapping + public RsData updateProfile(@AuthenticationPrincipal(expression = "id") Long userId, @Valid @RequestBody ProfileUpdateRequestDto profileUpdateRequestDto) { + ProfileResponseDto body = profileService.updateProfile(userId, profileUpdateRequestDto); + return RsData.successOf(body); // code=200 + } } From ba51b012fac175d0375474c2cb265311901eb8cb Mon Sep 17 00:00:00 2001 From: meohin Date: Thu, 18 Sep 2025 16:39:43 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20springdoc-openapi=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=ED=98=B8=ED=99=98=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5dc03649..61533ecf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") implementation("io.jsonwebtoken:jjwt-api:0.12.3") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")