Skip to content

Commit 567d3d5

Browse files
authored
Merge pull request #19 from Block-Guard/feat/#17/guardian-api
[Feat] Guardian API
2 parents 54cf6bb + be1aa38 commit 567d3d5

File tree

14 files changed

+349
-9
lines changed

14 files changed

+349
-9
lines changed

src/main/java/com/blockguard/server/domain/auth/infra/JwtTokenProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ private Claims parseClaims(String accessToken) {
8989
}
9090
// 사용자 ID 가져오기
9191
public Long getUserIdFromToken(String accessToken) {
92-
Claims claims = parseClaims(accessToken);
92+
String token = accessToken.substring(7);
93+
Claims claims = parseClaims(token);
9394
return Long.valueOf(claims.getSubject()); // claims.getSubject()를 Long으로 변환
9495
}
9596
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.blockguard.server.domain.guardian.api;
2+
3+
import com.blockguard.server.domain.guardian.dto.request.CreateGuardianRequest;
4+
import com.blockguard.server.domain.guardian.dto.request.UpdateGuardianPrimaryRequest;
5+
import com.blockguard.server.domain.guardian.dto.response.GuardianResponse;
6+
import com.blockguard.server.domain.guardian.dto.response.GuardiansListResponse;
7+
import com.blockguard.server.domain.guardian.application.GuardianService;
8+
import com.blockguard.server.domain.user.domain.User;
9+
import com.blockguard.server.global.common.codes.SuccessCode;
10+
import com.blockguard.server.global.common.response.BaseResponse;
11+
import com.blockguard.server.global.config.resolver.CurrentUser;
12+
import com.blockguard.server.global.config.swagger.CustomExceptionDescription;
13+
import com.blockguard.server.global.config.swagger.SwaggerResponseDescription;
14+
import io.swagger.v3.oas.annotations.Operation;
15+
import io.swagger.v3.oas.annotations.Parameter;
16+
import jakarta.validation.Valid;
17+
import lombok.RequiredArgsConstructor;
18+
import org.springframework.http.MediaType;
19+
import org.springframework.http.ResponseEntity;
20+
import org.springframework.web.bind.annotation.*;
21+
22+
@RestController
23+
@RequiredArgsConstructor
24+
@RequestMapping("/api/guardians")
25+
public class GuardianApi {
26+
private final GuardianService guardianService;
27+
28+
@Operation(summary = "보호자 목록 조회")
29+
@CustomExceptionDescription(SwaggerResponseDescription.FIND_GUARDIANS_FAIL)
30+
@GetMapping
31+
public ResponseEntity<BaseResponse<GuardiansListResponse>> getGuardiansList(@Parameter(hidden = true) @CurrentUser User user) {
32+
GuardiansListResponse guardiansList = guardianService.getGuardiansList(user);
33+
return ResponseEntity.ok(BaseResponse.of(SuccessCode.GUARDIAN_LIST_RETRIEVED, guardiansList));
34+
}
35+
36+
@Operation(summary = "보호자 등록")
37+
@CustomExceptionDescription(SwaggerResponseDescription.CREATE_GUARDIAN_FAIL)
38+
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
39+
public ResponseEntity<BaseResponse<GuardianResponse>> createGuardian(@Parameter(hidden = true) @CurrentUser User user,
40+
@Valid CreateGuardianRequest createGuardianRequest) {
41+
GuardianResponse guardianResponse = guardianService.createGuardian(user, createGuardianRequest);
42+
return ResponseEntity.ok(BaseResponse.of(SuccessCode.GUARDIAN_REGISTERED, guardianResponse));
43+
}
44+
45+
@Operation(summary = "보호자 정보 수정")
46+
@CustomExceptionDescription(SwaggerResponseDescription.UPDATE_GUARDIAN_FAIL)
47+
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
48+
public ResponseEntity<BaseResponse<GuardianResponse>> updateGuardian(@Parameter(hidden = true) @CurrentUser User user,
49+
@PathVariable("id") Long guardianId,
50+
@Valid CreateGuardianRequest createGuardianRequest) {
51+
GuardianResponse guardianResponse = guardianService.updateGuardian(user, guardianId, createGuardianRequest);
52+
return ResponseEntity.ok(BaseResponse.of(SuccessCode.GUARDIAN_UPDATED, guardianResponse));
53+
}
54+
55+
@Operation(summary = "보호자 대표 여부 수정")
56+
@CustomExceptionDescription(SwaggerResponseDescription.UPDATE_GUARDIAN_PRIMARY_FAIL)
57+
@PatchMapping(value = "/{id}/primary")
58+
public ResponseEntity<BaseResponse<GuardianResponse>> updatePrimary(@Parameter(hidden = true) @CurrentUser User user,
59+
@PathVariable("id") Long guardianId,
60+
@Valid @RequestBody UpdateGuardianPrimaryRequest updateGuardianPrimaryRequest) {
61+
GuardianResponse guardianResponse = guardianService.updatePrimary(user, guardianId, updateGuardianPrimaryRequest);
62+
return ResponseEntity.ok(BaseResponse.of(SuccessCode.GUARDIAN_PRIMARY_UPDATED, guardianResponse));
63+
}
64+
65+
@Operation(summary = "보호자 삭제")
66+
@CustomExceptionDescription(SwaggerResponseDescription.UPDATE_GUARDIAN_PRIMARY_FAIL)
67+
@DeleteMapping(value = "/{id}")
68+
public ResponseEntity<BaseResponse<Void>> deleteGuardian(@Parameter(hidden = true) @CurrentUser User user,
69+
@PathVariable("id") Long guardianId) {
70+
guardianService.deleteGuardian(user, guardianId);
71+
return ResponseEntity.ok(BaseResponse.of(SuccessCode.GUARDIAN_DELETED));
72+
}
73+
74+
75+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.blockguard.server.domain.guardian.application;
2+
3+
import com.blockguard.server.domain.guardian.dao.GuardianRepository;
4+
import com.blockguard.server.domain.guardian.domain.Guardian;
5+
import com.blockguard.server.domain.guardian.dto.request.CreateGuardianRequest;
6+
import com.blockguard.server.domain.guardian.dto.request.UpdateGuardianPrimaryRequest;
7+
import com.blockguard.server.domain.guardian.dto.response.GuardianResponse;
8+
import com.blockguard.server.domain.guardian.dto.response.GuardiansListResponse;
9+
import com.blockguard.server.domain.user.domain.User;
10+
import com.blockguard.server.global.common.codes.ErrorCode;
11+
import com.blockguard.server.global.config.S3.S3Service;
12+
import com.blockguard.server.global.exception.BusinessExceptionHandler;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class GuardianService {
20+
private final GuardianRepository guardianRepository;
21+
private final S3Service s3Service;
22+
23+
@Transactional
24+
public GuardiansListResponse getGuardiansList(User user) {
25+
return GuardiansListResponse.builder()
26+
.guardians(
27+
guardianRepository.findAllByUserId(user.getId()).stream()
28+
.map(guardian -> GuardianResponse.from(guardian, s3Service))
29+
.toList()
30+
)
31+
.build();
32+
33+
}
34+
35+
@Transactional
36+
public GuardianResponse createGuardian(User user, CreateGuardianRequest request) {
37+
String key = (request.getProfileImage() != null && !request.getProfileImage().isEmpty())
38+
? s3Service.upload(request.getProfileImage(), "guardians"): null;
39+
40+
Guardian newGuardian = Guardian.builder()
41+
.name(request.getName())
42+
.phoneNumber(request.getPhoneNumber())
43+
.profileImageKey(key)
44+
.user(user)
45+
.build();
46+
47+
Guardian saved = guardianRepository.save(newGuardian);
48+
49+
return GuardianResponse.from(saved, s3Service);
50+
}
51+
52+
@Transactional
53+
public GuardianResponse updateGuardian(User user, Long guardianId, CreateGuardianRequest request) {
54+
Guardian guardian = guardianRepository.findByIdAndUser(guardianId, user)
55+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.GUARDIAN_NOT_FOUND));
56+
57+
String key = null;
58+
if (request.getProfileImage() != null && !request.getProfileImage().isEmpty()){
59+
// 새 이미지 업로드
60+
key = s3Service.upload(request.getProfileImage(), "guardians");
61+
// 예전 이미지 삭제
62+
if (guardian.getProfileImageKey() != null){
63+
s3Service.delete(guardian.getProfileImageKey());
64+
}
65+
}
66+
guardian.updateGuardianInfo(request.getName(), request.getPhoneNumber(), key);
67+
68+
Guardian updated = guardianRepository.save(guardian);
69+
70+
return GuardianResponse.from(updated, s3Service);
71+
}
72+
73+
@Transactional
74+
public GuardianResponse updatePrimary(User user, Long guardianId, UpdateGuardianPrimaryRequest request) {
75+
Guardian guardian = guardianRepository.findByIdAndUser(guardianId, user)
76+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.GUARDIAN_NOT_FOUND));
77+
78+
// 다른 보호자 대표 해제
79+
if (request.isPrimary()) {
80+
guardianRepository.clearPrimaryFlagsByUser(user);
81+
}
82+
83+
guardian.setPrimary(request.isPrimary());
84+
Guardian updated = guardianRepository.save(guardian);
85+
86+
return GuardianResponse.from(updated, s3Service);
87+
}
88+
89+
@Transactional
90+
public void deleteGuardian(User user, Long guardianId) {
91+
Guardian guardian = guardianRepository.findByIdAndUser(guardianId, user)
92+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.GUARDIAN_NOT_FOUND));
93+
94+
guardian.markDeleted();
95+
}
96+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.blockguard.server.domain.guardian.dao;
2+
3+
import com.blockguard.server.domain.guardian.domain.Guardian;
4+
import com.blockguard.server.domain.user.domain.User;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
public interface GuardianRepository extends JpaRepository<Guardian, Long> {
13+
List<Guardian> findAllByUserId(Long userId);
14+
Optional<Guardian> findByIdAndUser(Long id, User user);
15+
16+
@Modifying
17+
@Query("""
18+
UPDATE Guardian g
19+
SET g.isPrimary = false
20+
WHERE g.user = :user
21+
AND g.deletedAt IS NULL
22+
AND g.isPrimary = true
23+
""")
24+
void clearPrimaryFlagsByUser(User user);
25+
}

src/main/java/com/blockguard/server/domain/guardian/domain/Guardian.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ public class Guardian extends BaseEntity {
4444
public void markDeleted() {
4545
this.deletedAt = LocalDateTime.now();
4646
}
47+
48+
public void updateGuardianInfo(String name, String phoneNumber, String profileImageKey) {
49+
this.name = name;
50+
this.phoneNumber = phoneNumber;
51+
this.profileImageKey = profileImageKey;
52+
}
53+
54+
public void setPrimary(boolean isPrimary) {
55+
this.isPrimary = isPrimary;
56+
}
4757
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.blockguard.server.domain.guardian.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import org.springframework.web.multipart.MultipartFile;
10+
11+
@Getter
12+
@AllArgsConstructor
13+
@Builder
14+
public class CreateGuardianRequest {
15+
@NotBlank
16+
private String name;
17+
18+
@NotBlank
19+
@Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "010-1234-5678 형식이어야 합니다.")
20+
private String phoneNumber;
21+
22+
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED)
23+
private MultipartFile profileImage;
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.blockguard.server.domain.guardian.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Builder
14+
@JsonIgnoreProperties("primary")
15+
public class UpdateGuardianPrimaryRequest {
16+
@JsonProperty("isPrimary")
17+
private boolean isPrimary;
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.blockguard.server.domain.guardian.dto.response;
2+
3+
import com.blockguard.server.domain.guardian.domain.Guardian;
4+
import com.blockguard.server.global.config.S3.S3Service;
5+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import lombok.AllArgsConstructor;
9+
import lombok.Builder;
10+
import lombok.Getter;
11+
12+
@Getter
13+
@AllArgsConstructor
14+
@Builder
15+
@JsonIgnoreProperties("primary") // 자동으로 생성되는 "primary" 프로퍼티 무시
16+
public class GuardianResponse {
17+
private Long guardiansId;
18+
private String name;
19+
20+
@Schema(description = "전화번호 (010-1234-5678 형식)", example = "010-1234-5678")
21+
private String phoneNumber;
22+
23+
@JsonProperty("isPrimary")
24+
private boolean isPrimary;
25+
private String profileImageUrl;
26+
private String createdAt;
27+
28+
public static GuardianResponse from(Guardian guardian, S3Service s3Service) {
29+
return GuardianResponse.builder()
30+
.guardiansId(guardian.getId())
31+
.name(guardian.getName())
32+
.phoneNumber(guardian.getPhoneNumber())
33+
.isPrimary(guardian.isPrimary())
34+
.createdAt(guardian.getCreatedAt().toString())
35+
.profileImageUrl(
36+
guardian.getProfileImageKey() != null
37+
? s3Service.getPublicUrl(guardian.getProfileImageKey())
38+
: null
39+
)
40+
.build();
41+
}
42+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.blockguard.server.domain.guardian.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@AllArgsConstructor
11+
@Builder
12+
public class GuardiansListResponse {
13+
private List<GuardianResponse> guardians;
14+
}

src/main/java/com/blockguard/server/domain/user/application/UserService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ public void updateUserInfo(Long id, UpdateUserInfo updateUserInfo) {
5050
}
5151
}
5252
if (updateUserInfo.getProfileImage() != null && !updateUserInfo.getProfileImage().isEmpty()) {
53-
String contentType = updateUserInfo.getProfileImage().getContentType();
54-
if (contentType == null || !contentType.startsWith("image/")) {
55-
throw new BusinessExceptionHandler(ErrorCode.INVALID_PROFILE_IMAGE);
56-
}
5753
String imageKey = s3Service.upload(updateUserInfo.getProfileImage(), "profiles");
5854
user.updateProfileImageKey(imageKey);
5955
}

0 commit comments

Comments
 (0)