diff --git a/src/main/java/Devroup/hidaddy/controller/UserController.java b/src/main/java/Devroup/hidaddy/controller/UserController.java index 7007e6b..1a41fd3 100644 --- a/src/main/java/Devroup/hidaddy/controller/UserController.java +++ b/src/main/java/Devroup/hidaddy/controller/UserController.java @@ -1,8 +1,12 @@ package Devroup.hidaddy.controller; import Devroup.hidaddy.dto.user.*; +import Devroup.hidaddy.entity.Baby; +import Devroup.hidaddy.entity.User; import Devroup.hidaddy.security.UserDetailsImpl; +import Devroup.hidaddy.service.BabyService; import Devroup.hidaddy.service.UserService; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; @@ -14,9 +18,12 @@ import org.springframework.web.bind.annotation.*; import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.LocalDate; +import java.util.List; + @Tag( name = "User", - description = "회원 정보, 사용자, 아기 관련 API" + description = "회원 정보, 아기 등록 및 선택 등 사용자 관련 API" ) @RestController @RequiredArgsConstructor @@ -24,6 +31,7 @@ public class UserController { private final UserService userService; + private final BabyService babyService; @GetMapping("") @Operation( @@ -68,6 +76,7 @@ public ResponseEntity changeName( return ResponseEntity.ok("유저 이름 변경 완료"); } + // 아기 등록 (튜토리얼) @Operation(summary = "아기 정보 등록 (튜토리얼)", description = "로그인한 사용자의 이름과 아기 정보를 등록하고, 선택된 아기 ID도 자동으로 저장합니다.") @ApiResponses({ @@ -87,27 +96,101 @@ public ResponseEntity registerBaby( return ResponseEntity.ok("아기 정보 등록 완료"); } + // 전체 아기 목록 조회 + @Operation(summary = "전체 아기 목록 조회", + description = "로그인된 사용자의 전체 아기 정보를 반환합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "아기 목록 조회 성공"), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자 (로그인 필요)") + }) @GetMapping("/baby") - @Operation( - summary = "선택된 아기 정보 조회", - description = "로그인된 사용자가 선택한 아기의 상세 정보(이름, D-day, 프로필 이미지, 메시지)를 반환합니다. " - + "Authorization 헤더에 유효한 Access Token이 필요합니다." - ) + public ResponseEntity> getAllBabies( + @AuthenticationPrincipal UserDetailsImpl userDetails) { + + if (userDetails == null) { + return ResponseEntity.status(401).build(); + } + + List babies = babyService.getBabies(userDetails.getUser()); + return ResponseEntity.ok(babies); + } + + // 특정 아기 정보 조회 + @Operation(summary = "특정 아기 정보 조회", + description = "지정된 아기 ID에 해당하는 정보를 반환합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "아기 정보 조회 성공"), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자 (로그인 필요)"), + @ApiResponse(responseCode = "403", description = "권한 없음"), + @ApiResponse(responseCode = "404", description = "해당 아기 없음") + }) + @GetMapping("/baby/{babyId}") + public ResponseEntity getBaby( + @PathVariable Long babyId, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + + if (userDetails == null) return ResponseEntity.status(401).build(); + + BabyResponse baby = babyService.getBaby(userDetails.getUser(), babyId); + return ResponseEntity.ok(baby); + } + + // 아기 수정 (이름, 날짜, 이미지) + @PatchMapping(value = "/baby/{babyId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "아기 정보 수정 (JSON + 이미지 Multipart)", description = "지정된 아기의 이름, 출산 예정일, 프로필 이미지 등을 수정합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청") + }) + public ResponseEntity updateBaby( + @PathVariable Long babyId, + @RequestParam("name") String name, + @RequestParam("dueDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate dueDate, + @RequestPart(value = "image", required = false) MultipartFile image, + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(401).build(); + } + + BabyUpdateRequest dto = new BabyUpdateRequest(); + dto.setName(name); + dto.setDueDate(dueDate); + + BabyResponse response = babyService.updateBaby(userDetails.getUser(), babyId, dto, image); + return ResponseEntity.ok(response); + } + + + // 아기 삭제 + @Operation(summary = "아기 삭제", description = "지정된 아기 정보를 삭제합니다.") + @DeleteMapping("/baby/{babyId}") + public ResponseEntity deleteBaby(@PathVariable Long babyId, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + if (userDetails == null) return ResponseEntity.status(401).build(); + babyService.deleteBaby(userDetails.getUser(), babyId); + return ResponseEntity.ok("아기 삭제 완료"); + } + + @Operation(summary = "아기 정보 등록", description = "태명과 출산 예정일만 입력하여 아기를 등록합니다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "선택된 아기 정보 조회 성공"), + @ApiResponse(responseCode = "200", description = "아기 정보 등록 성공"), @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자 (로그인 필요)"), - @ApiResponse(responseCode = "400", description = "선택된 아기가 없거나 해당 아기를 찾을 수 없음") + @ApiResponse(responseCode = "400", description = "잘못된 요청") }) - public ResponseEntity getSelectedBabyInfo(@AuthenticationPrincipal UserDetailsImpl userDetails) { + @PostMapping("/baby/basic") + public ResponseEntity registerBabySimple( + @RequestBody BabyBasicRegisterRequest request, + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { if (userDetails == null) { return ResponseEntity.status(401).build(); } - SelectedBabyResponse selectedBabyInfo = userService.getSelectedBabyInfo(userDetails.getUser()); - return ResponseEntity.ok(selectedBabyInfo); + BabyResponse response = babyService.registerBabyBasic(request, userDetails.getUser()); + return ResponseEntity.ok(response); } - @PatchMapping("/selected-baby/{babyId}") @Operation(summary = "선택된 아기 변경", description = "로그인한 사용자가 등록한 아기 중 하나를 선택된 아기로 변경합니다.") @ApiResponses({ @@ -115,14 +198,15 @@ public ResponseEntity getSelectedBabyInfo(@AuthenticationP @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자 (로그인 필요)"), @ApiResponse(responseCode = "404", description = "해당 아기 정보를 찾을 수 없음") }) + @PatchMapping("/select-baby/{babyId}") public ResponseEntity selectBaby(@PathVariable Long babyId, @AuthenticationPrincipal UserDetailsImpl userDetails) { if (userDetails == null) { return ResponseEntity.status(401).body("인증이 필요합니다."); } - userService.changeSelectedBaby(userDetails.getUser(), babyId); - return ResponseEntity.ok("선택된 아기 변경 완료"); + BabyResponse response = userService.changeSelectedBaby(userDetails.getUser(), babyId); + return ResponseEntity.ok(response); } @PatchMapping(value = "/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -139,7 +223,7 @@ public ResponseEntity selectBaby(@PathVariable Long babyId, public ResponseEntity uploadProfileImage( @RequestPart(value = "image", required = true) MultipartFile image, @AuthenticationPrincipal UserDetailsImpl userDetails) { - + if (userDetails == null) { return ResponseEntity.status(401).body("인증이 필요합니다."); } @@ -147,4 +231,24 @@ public ResponseEntity uploadProfileImage( String imageUrl = userService.uploadProfileImage(userDetails.getUser(), image); return ResponseEntity.ok(imageUrl); } + + @Operation( + summary = "사용자 및 파트너 전화번호 등록/수정", + description = "로그인된 사용자의 `phone`과 `partnerphonee`을 등록하거나 수정합니다. " + + "요청 시 JSON 바디로 `phone`, `partnerphone` 값을 전달하며, " + + "둘 중 하나만 보내도 되고, 보내지 않은 필드는 기존 값을 유지합니다. " + + "Authorization 헤더에 유효한 Access Token이 필요합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "전화번호 등록/수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 (전화번호 형식 오류 등)"), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자 (로그인 필요)"), + @ApiResponse(responseCode = "404", description = "해당 유저를 찾을 수 없음") + }) + @PatchMapping("/phone") + public ResponseEntity patchPhoneNumbers(@AuthenticationPrincipal UserDetailsImpl userDetails, + @RequestBody PhoneUpdateRequest dto) { + userService.updatePhoneNumbers(userDetails.getUser().getId(), dto); + return ResponseEntity.ok("전화번호가 성공적으로 변경되었습니다."); + } } diff --git a/src/main/java/Devroup/hidaddy/dto/user/BabyBasicRegisterRequest.java b/src/main/java/Devroup/hidaddy/dto/user/BabyBasicRegisterRequest.java new file mode 100644 index 0000000..9aea159 --- /dev/null +++ b/src/main/java/Devroup/hidaddy/dto/user/BabyBasicRegisterRequest.java @@ -0,0 +1,20 @@ +package Devroup.hidaddy.dto.user; + +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +public class BabyBasicRegisterRequest { + + private String babyName; // 태명 + private String dueDate; // 예: "2025-12-25" (yyyy-MM-dd 형식) + + public LocalDateTime getParsedDueDate() { + if (dueDate == null || dueDate.isBlank()) { + throw new IllegalArgumentException("출산 예정일은 필수입니다."); + } + return LocalDate.parse(dueDate).atStartOfDay(); + } +} diff --git a/src/main/java/Devroup/hidaddy/dto/user/BabyResponse.java b/src/main/java/Devroup/hidaddy/dto/user/BabyResponse.java new file mode 100644 index 0000000..fd5e5ba --- /dev/null +++ b/src/main/java/Devroup/hidaddy/dto/user/BabyResponse.java @@ -0,0 +1,25 @@ +package Devroup.hidaddy.dto.user; + +import Devroup.hidaddy.entity.Baby; +import lombok.Getter; +import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +public class BabyResponse { + private Long id; + private String name; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") // 날짜 포맷 지정 + private LocalDate dueDate; + private String profileImage; + + public BabyResponse(Baby baby) { + this.id = baby.getId(); + this.name = baby.getName(); + this.dueDate = baby.getDueDate().toLocalDate(); + this.profileImage = baby.getBabyImageUrl(); // 있으면 가져오기 + } +} diff --git a/src/main/java/Devroup/hidaddy/dto/user/BabyUpdateRequest.java b/src/main/java/Devroup/hidaddy/dto/user/BabyUpdateRequest.java new file mode 100644 index 0000000..64dcb2c --- /dev/null +++ b/src/main/java/Devroup/hidaddy/dto/user/BabyUpdateRequest.java @@ -0,0 +1,15 @@ +package Devroup.hidaddy.dto.user; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Setter +public class BabyUpdateRequest { + private String name; + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate dueDate; +} diff --git a/src/main/java/Devroup/hidaddy/dto/user/PhoneUpdateRequest.java b/src/main/java/Devroup/hidaddy/dto/user/PhoneUpdateRequest.java new file mode 100644 index 0000000..da019c1 --- /dev/null +++ b/src/main/java/Devroup/hidaddy/dto/user/PhoneUpdateRequest.java @@ -0,0 +1,11 @@ +package Devroup.hidaddy.dto.user; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PhoneUpdateRequest { + private String Phone; + private String partnerPhone; +} \ No newline at end of file diff --git a/src/main/java/Devroup/hidaddy/entity/Baby.java b/src/main/java/Devroup/hidaddy/entity/Baby.java index 3ec3803..d6798e0 100644 --- a/src/main/java/Devroup/hidaddy/entity/Baby.java +++ b/src/main/java/Devroup/hidaddy/entity/Baby.java @@ -1,15 +1,13 @@ package Devroup.hidaddy.entity; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Builder; -import lombok.AllArgsConstructor; +import lombok.*; import jakarta.persistence.*; import java.time.LocalDateTime; @Entity // JPA가 이 클래스를 엔티티로 인식하도록 지정하는 어노테이션 @Table(name = "baby") // 데이터베이스에서 사용될 테이블 이름을 'baby'로 지정 @Getter // Lombok: 모든 필드에 대한 getter 메소드를 자동 생성 +@Setter @NoArgsConstructor // Lombok: 파라미터가 없는 기본 생성자를 자동 생성 @AllArgsConstructor // Lombok: 모든 필드를 파라미터로 받는 생성자를 자동 생성 @Builder // Lombok: 빌더 패턴을 사용하여 객체를 생성할 수 있도록 지원 diff --git a/src/main/java/Devroup/hidaddy/service/BabyService.java b/src/main/java/Devroup/hidaddy/service/BabyService.java new file mode 100644 index 0000000..9a90ee8 --- /dev/null +++ b/src/main/java/Devroup/hidaddy/service/BabyService.java @@ -0,0 +1,105 @@ +package Devroup.hidaddy.service; + +import Devroup.hidaddy.dto.user.BabyBasicRegisterRequest; +import Devroup.hidaddy.dto.user.BabyResponse; +import Devroup.hidaddy.dto.user.BabyUpdateRequest; +import Devroup.hidaddy.entity.Baby; +import Devroup.hidaddy.entity.User; +import Devroup.hidaddy.repository.user.BabyRepository; +import Devroup.hidaddy.repository.user.UserRepository; +import Devroup.hidaddy.util.S3Uploader; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BabyService { + + private final UserRepository userRepository; + private final BabyRepository babyRepository; + private final S3Uploader s3Uploader; + + @Value("${cloudfront.domain}") + private String cloudFrontDomain; + + // 아기 리스트 조회 + public List getBabies(User user) { + List babies = babyRepository.findByUser(user); + return babies.stream() + .map(BabyResponse::new) + .toList(); + } + + // 아기 조회 + public BabyResponse getBaby(User user, Long babyId) { + Baby baby = babyRepository.findById(babyId) + .orElseThrow(() -> new IllegalArgumentException("해당 아기를 찾을 수 없습니다.")); + if (!baby.getUser().getId().equals(user.getId())) { + throw new IllegalArgumentException("해당 아기에 대한 접근 권한이 없습니다."); + } + return new BabyResponse(baby); + } + + // 아기 수정 + @Transactional + public BabyResponse updateBaby(User user, Long babyId, BabyUpdateRequest dto, MultipartFile image) { + Baby baby = babyRepository.findById(babyId) + .orElseThrow(() -> new IllegalArgumentException("해당 아기를 찾을 수 없습니다.")); + + if (!baby.getUser().getId().equals(user.getId())) { + throw new IllegalArgumentException("해당 아기에 대한 수정 권한이 없습니다."); + } + + if (dto.getName() != null) baby.setName(dto.getName()); + if (dto.getDueDate() != null) baby.setDueDate(dto.getDueDate().atStartOfDay()); + + if (image != null && !image.isEmpty()) { + // 기존 이미지 삭제 + if (baby.getBabyImageUrl() != null && !baby.getBabyImageUrl().isEmpty()) { + String imageKey = baby.getBabyImageUrl().replace(cloudFrontDomain + "/", ""); + s3Uploader.delete(imageKey); + } + + // 새 이미지 업로드 + String imageUrl = s3Uploader.upload(image, "baby-profile"); + imageUrl = cloudFrontDomain + "/" + imageUrl; + baby.setBabyImageUrl(imageUrl); + } + + return new BabyResponse(babyRepository.save(baby)); + } + + // 아기 삭제 + public void deleteBaby(User user, Long babyId) { + Baby baby = babyRepository.findById(babyId) + .orElseThrow(() -> new IllegalArgumentException("해당 아기를 찾을 수 없습니다.")); + if (!baby.getUser().getId().equals(user.getId())) { + throw new IllegalArgumentException("해당 아기에 대한 삭제 권한이 없습니다."); + } + babyRepository.delete(baby); + } + + @Transactional + public BabyResponse registerBabyBasic(BabyBasicRegisterRequest request, User user) { + // 아기 생성 + Baby baby = Baby.builder() + .name(request.getBabyName()) + .dueDate(request.getParsedDueDate()) + .user(user) + .build(); + + // 저장 + babyRepository.save(baby); + + // 선택된 아기로 지정 + user.setSelectedBabyId(baby.getId()); + userRepository.save(user); + + return new BabyResponse(baby); + } +} diff --git a/src/main/java/Devroup/hidaddy/service/UserService.java b/src/main/java/Devroup/hidaddy/service/UserService.java index 48c8e8a..80f7504 100644 --- a/src/main/java/Devroup/hidaddy/service/UserService.java +++ b/src/main/java/Devroup/hidaddy/service/UserService.java @@ -25,7 +25,7 @@ public class UserService { private final UserRepository userRepository; private final BabyRepository babyRepository; private final S3Uploader s3Uploader; - + @Value("${cloudfront.domain}") private String cloudFrontDomain; @@ -50,7 +50,7 @@ public void registerBaby(BabyRegisterRequest dto, User user) { userRepository.save(user); } - public void changeSelectedBaby(User user, Long babyId) { + public BabyResponse changeSelectedBaby(User user, Long babyId) { Baby baby = babyRepository.findById(babyId) .orElseThrow(() -> new IllegalArgumentException("해당 아기를 찾을 수 없습니다.")); @@ -61,6 +61,8 @@ public void changeSelectedBaby(User user, Long babyId) { user.setSelectedBabyId(babyId); userRepository.save(user); + + return new BabyResponse(baby); } public void changeUserName(User user, String userName) { @@ -175,4 +177,18 @@ public String uploadProfileImage(User user, MultipartFile image) { return imageUrl; } + + public void updatePhoneNumbers(Long userId, PhoneUpdateRequest dto) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + if (dto.getPhone() != null) { + user.setPhone(dto.getPhone()); + } + if (dto.getPartnerPhone() != null) { + user.setPartnerPhone(dto.getPartnerPhone()); + } + + userRepository.save(user); + } }