Skip to content

Commit 07521df

Browse files
lian2945Copilot
andcommitted
feat/47 :: 크리에이터 프로필 조회/수정 API 구현
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0cbc69d commit 07521df

78 files changed

Lines changed: 2213 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

services/creator/build.gradle

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
1+
plugins {
2+
id 'com.google.protobuf' version '0.9.6'
3+
}
4+
5+
ext {
6+
springGrpcVersion = "1.0.2"
7+
springCloudVersion = "2025.1.0"
8+
}
19
version = '0.0.1'
210
description = 'creator'
311

412
dependencies {
13+
implementation 'org.springframework.boot:spring-boot-starter-web'
14+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
15+
implementation 'org.springframework.grpc:spring-grpc-client-spring-boot-starter'
16+
implementation 'org.springframework.grpc:spring-grpc-server-spring-boot-starter'
17+
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
18+
implementation 'org.springframework.boot:spring-boot-starter-kafka'
19+
testImplementation 'org.springframework.kafka:spring-kafka-test'
20+
runtimeOnly 'com.mysql:mysql-connector-j'
21+
22+
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
23+
}
24+
dependencyManagement {
25+
imports {
26+
mavenBom "org.springframework.grpc:spring-grpc-dependencies:$springGrpcVersion"
27+
mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion"
28+
}
29+
}
30+
31+
// protobuf 설정
32+
protobuf {
33+
protoc {
34+
artifact = "com.google.protobuf:protoc:4.34.0"
35+
}
36+
plugins {
37+
grpc {
38+
artifact = 'io.grpc:protoc-gen-grpc-java:1.79.0'
39+
}
40+
}
41+
generateProtoTasks {
42+
all()*.plugins {
43+
grpc {}
44+
}
45+
}
546
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package kr.magicbox.creator.adapter.in.security.configuration;
2+
3+
import kr.magicbox.creator.adapter.in.security.filter.UserInfoExtractFilter;
4+
import kr.magicbox.creator.adapter.in.security.properties.TrustedIpProperties;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
11+
import org.springframework.security.config.http.SessionCreationPolicy;
12+
import org.springframework.security.core.userdetails.UserDetailsService;
13+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
14+
import org.springframework.security.web.SecurityFilterChain;
15+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
16+
17+
@Configuration
18+
@EnableWebSecurity
19+
@RequiredArgsConstructor
20+
public class SecurityConfiguration {
21+
22+
private final TrustedIpProperties trustedIpProperties;
23+
24+
@Bean
25+
public UserDetailsService userDetailsService() {
26+
return new InMemoryUserDetailsManager();
27+
}
28+
29+
@Bean
30+
public SecurityFilterChain filterChain(HttpSecurity http) {
31+
return http
32+
.csrf(AbstractHttpConfigurer::disable)
33+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
34+
.addFilterBefore(new UserInfoExtractFilter(trustedIpProperties), UsernamePasswordAuthenticationFilter.class)
35+
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
36+
.build();
37+
}
38+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package kr.magicbox.creator.adapter.in.security.filter;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import kr.magicbox.creator.adapter.in.security.properties.TrustedIpProperties;
8+
import kr.magicbox.creator.domain.vo.UserId;
9+
import lombok.NonNull;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13+
import org.springframework.security.core.context.SecurityContextHolder;
14+
import org.springframework.web.filter.OncePerRequestFilter;
15+
16+
import java.io.IOException;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
public class UserInfoExtractFilter extends OncePerRequestFilter {
21+
22+
private final TrustedIpProperties trustedIpProperties;
23+
24+
@Override
25+
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
26+
String clientIp = request.getRemoteAddr();
27+
28+
if (!trustedIpProperties.getIps().contains(clientIp)) {
29+
filterChain.doFilter(request, response);
30+
return;
31+
}
32+
33+
String userIdRequestHeader = request.getHeader("X-User-Id");
34+
35+
if (!isValidUserId(userIdRequestHeader)) {
36+
filterChain.doFilter(request, response);
37+
return;
38+
}
39+
40+
Long userIdLong = Long.valueOf(userIdRequestHeader);
41+
UserId userId = UserId.of(userIdLong);
42+
log.info(String.valueOf(userId));
43+
44+
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userId, null);
45+
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
46+
47+
filterChain.doFilter(request, response);
48+
}
49+
50+
private boolean isValidUserId(String userIdRequestHeader) {
51+
try {
52+
return Long.parseLong(userIdRequestHeader) > 0;
53+
}
54+
catch (Exception e) {
55+
return false;
56+
}
57+
}
58+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package kr.magicbox.creator.adapter.in.security.properties;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@RequiredArgsConstructor
11+
@ConfigurationProperties(prefix = "security.trusted")
12+
public class TrustedIpProperties {
13+
14+
private final List<String> ips;
15+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package kr.magicbox.creator.adapter.in.web;
2+
3+
import kr.magicbox.creator.adapter.in.web.dto.request.UpdateCreatorProfileRequest;
4+
import kr.magicbox.creator.application.dto.command.WithdrawCreatorCommand;
5+
import kr.magicbox.creator.application.port.in.UpdateCreatorProfileUseCase;
6+
import kr.magicbox.creator.application.port.in.WithdrawCreatorUseCase;
7+
import kr.magicbox.creator.domain.vo.UserId;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
@RequestMapping("/api/creator")
16+
@RequiredArgsConstructor
17+
public class CreatorCommandController {
18+
19+
private final UpdateCreatorProfileUseCase updateCreatorProfileUseCase;
20+
private final WithdrawCreatorUseCase withdrawCreatorUseCase;
21+
22+
@PatchMapping("/profile")
23+
public ResponseEntity<Void> updateProfile(
24+
@AuthenticationPrincipal UserId userId,
25+
@Valid @RequestBody UpdateCreatorProfileRequest request
26+
) {
27+
updateCreatorProfileUseCase.updateCreatorProfile(request.toCommand(userId));
28+
return ResponseEntity.noContent().build();
29+
}
30+
31+
@DeleteMapping
32+
public ResponseEntity<Void> withdrawCreator(@AuthenticationPrincipal UserId userId) {
33+
withdrawCreatorUseCase.withdrawCreator(WithdrawCreatorCommand.of(userId));
34+
return ResponseEntity.noContent().build();
35+
}
36+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package kr.magicbox.creator.adapter.in.web;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import kr.magicbox.creator.adapter.in.web.constants.CursorConstants;
5+
import kr.magicbox.creator.adapter.in.web.dto.response.CreatorMyProfileResponse;
6+
import kr.magicbox.creator.adapter.in.web.dto.response.CreatorProfileResponse;
7+
import kr.magicbox.creator.adapter.in.web.dto.response.CreatorSearchResponse;
8+
import kr.magicbox.creator.adapter.in.web.dto.response.CursorResponse;
9+
import kr.magicbox.creator.adapter.in.web.validation.CursorSize;
10+
import kr.magicbox.creator.application.dto.result.CreatorPublicProfileResult;
11+
import kr.magicbox.creator.application.dto.query.GetAllCreatorsQuery;
12+
import kr.magicbox.creator.application.dto.query.GetCreatorProfileQuery;
13+
import kr.magicbox.creator.application.dto.query.GetMyCreatorProfileQuery;
14+
import kr.magicbox.creator.application.dto.query.SearchCreatorsQuery;
15+
import kr.magicbox.creator.application.port.in.GetAllCreatorsUseCase;
16+
import kr.magicbox.creator.application.port.in.GetCreatorProfileUseCase;
17+
import kr.magicbox.creator.application.port.in.GetMyCreatorProfileUseCase;
18+
import kr.magicbox.creator.application.port.in.SearchCreatorsUseCase;
19+
import kr.magicbox.creator.domain.vo.Nickname;
20+
import kr.magicbox.creator.domain.vo.UserId;
21+
import lombok.RequiredArgsConstructor;
22+
import org.springframework.http.ResponseEntity;
23+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
24+
import org.springframework.validation.annotation.Validated;
25+
import org.springframework.web.bind.annotation.GetMapping;
26+
import org.springframework.web.bind.annotation.PathVariable;
27+
import org.springframework.web.bind.annotation.RequestMapping;
28+
import org.springframework.web.bind.annotation.RequestParam;
29+
import org.springframework.web.bind.annotation.RestController;
30+
31+
import java.util.List;
32+
33+
@RestController
34+
@RequestMapping("/api/creator")
35+
@RequiredArgsConstructor
36+
@Validated
37+
public class CreatorQueryController {
38+
39+
private final GetCreatorProfileUseCase getCreatorProfileUseCase;
40+
private final GetMyCreatorProfileUseCase getMyCreatorProfileUseCase;
41+
private final GetAllCreatorsUseCase getAllCreatorsUseCase;
42+
private final SearchCreatorsUseCase searchCreatorsUseCase;
43+
44+
@GetMapping("/profile/{nickname}")
45+
public ResponseEntity<CreatorProfileResponse> getProfile(
46+
@AuthenticationPrincipal UserId userId,
47+
@PathVariable String nickname
48+
) {
49+
CreatorPublicProfileResult result = getCreatorProfileUseCase.getCreatorProfile(
50+
GetCreatorProfileQuery.of(Nickname.of(nickname), userId)
51+
);
52+
return ResponseEntity.ok(CreatorProfileResponse.from(result));
53+
}
54+
55+
@GetMapping("/profile/me")
56+
public ResponseEntity<CreatorMyProfileResponse> getMyProfile(
57+
@AuthenticationPrincipal UserId userId
58+
) {
59+
return ResponseEntity.ok(CreatorMyProfileResponse.from(
60+
getMyCreatorProfileUseCase.getMyCreatorProfile(GetMyCreatorProfileQuery.of(userId))
61+
));
62+
}
63+
64+
@GetMapping
65+
public ResponseEntity<CursorResponse<CreatorSearchResponse>> getAllCreators(
66+
@RequestParam(required = false) Long cursor,
67+
@RequestParam(defaultValue = CursorConstants.DEFAULT_SIZE) @CursorSize Integer size) {
68+
List<CreatorSearchResponse> content = getAllCreatorsUseCase.getAllCreators(GetAllCreatorsQuery.of(cursor, size + 1))
69+
.stream()
70+
.map(CreatorSearchResponse::from)
71+
.toList();
72+
return ResponseEntity.ok(CursorResponse.of(content, size, CreatorSearchResponse::creatorId));
73+
}
74+
75+
@GetMapping("/search")
76+
public ResponseEntity<CursorResponse<CreatorSearchResponse>> searchCreators(
77+
@RequestParam @NotBlank(message = "닉네임은 필수입니다.") String nickname,
78+
@RequestParam(required = false) Long cursor,
79+
@RequestParam(defaultValue = CursorConstants.DEFAULT_SIZE) @CursorSize Integer size) {
80+
List<CreatorSearchResponse> content = searchCreatorsUseCase.searchCreators(SearchCreatorsQuery.of(nickname, cursor, size + 1))
81+
.stream()
82+
.map(CreatorSearchResponse::from)
83+
.toList();
84+
return ResponseEntity.ok(CursorResponse.of(content, size, CreatorSearchResponse::creatorId));
85+
}
86+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package kr.magicbox.creator.adapter.in.web.constants;
2+
3+
import lombok.AccessLevel;
4+
import lombok.NoArgsConstructor;
5+
6+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
7+
public final class CursorConstants {
8+
public static final String DEFAULT_SIZE = "20";
9+
public static final int MIN_SIZE = 5;
10+
public static final int MAX_SIZE = 100;
11+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package kr.magicbox.creator.adapter.in.web.dto.request;
2+
3+
import jakarta.validation.constraints.NotEmpty;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Size;
6+
import kr.magicbox.creator.application.dto.command.UpdateCreatorProfileCommand;
7+
import kr.magicbox.creator.domain.constants.CreatorPolicyConstants;
8+
import kr.magicbox.creator.domain.enums.MagicGenre;
9+
import kr.magicbox.creator.domain.vo.Nickname;
10+
import kr.magicbox.creator.domain.vo.UserId;
11+
import lombok.Builder;
12+
13+
import java.util.Set;
14+
15+
@Builder
16+
public record UpdateCreatorProfileRequest(
17+
@Size(min = CreatorPolicyConstants.nicknameMinLength, max = CreatorPolicyConstants.nicknameMaxLength) String nickname,
18+
@Size(max = 50) String tagline,
19+
String profileImageUrl,
20+
@Size(max = 500) String introduction,
21+
@NotEmpty Set<@NotNull MagicGenre> genres
22+
) {
23+
24+
public UpdateCreatorProfileCommand toCommand(UserId userId) {
25+
return new UpdateCreatorProfileCommand(
26+
userId,
27+
Nickname.of(nickname),
28+
tagline,
29+
profileImageUrl,
30+
introduction,
31+
genres
32+
);
33+
}
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package kr.magicbox.creator.adapter.in.web.dto.response;
2+
3+
import kr.magicbox.creator.application.dto.result.CreatorMyProfileResult;
4+
import kr.magicbox.creator.application.dto.result.ReviewRating;
5+
import lombok.Builder;
6+
7+
import java.util.List;
8+
9+
@Builder
10+
public record CreatorMyProfileResponse(
11+
String nickname,
12+
String tagline,
13+
long subscriberCount,
14+
long releaseCount,
15+
ReviewRating reviewRating,
16+
List<Object> releases,
17+
List<Object> shortForms,
18+
String introduction
19+
) {
20+
21+
public static CreatorMyProfileResponse from(CreatorMyProfileResult result) {
22+
return CreatorMyProfileResponse.builder()
23+
.nickname(result.nickname())
24+
.tagline(result.tagline())
25+
.subscriberCount(result.subscriberCount())
26+
.releaseCount(result.releaseCount())
27+
.reviewRating(result.reviewRating())
28+
.releases(result.releases())
29+
.shortForms(result.shortForms())
30+
.introduction(result.introduction())
31+
.build();
32+
}
33+
}

0 commit comments

Comments
 (0)