Skip to content

Commit

Permalink
Merge pull request #287 from JiHongKim98/feature/user-inquiries
Browse files Browse the repository at this point in the history
회원 1:1 문의하기 기능 추가
  • Loading branch information
junseoparkk authored Oct 7, 2024
2 parents 128db3b + e75c836 commit 149b75f
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_REGION=ap-northeast-2
S3_IMAGE_URL=

# DISCORD
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890987654/abcdefg1234567890
25 changes: 25 additions & 0 deletions src/main/java/com/example/daobe/common/config/DiscordConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.daobe.common.config;

import com.example.daobe.user.infrastructure.discord.DiscordApiClient;
import com.example.daobe.user.infrastructure.discord.DiscordProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
@RequiredArgsConstructor
public class DiscordConfig {

private final DiscordProperties discordProperties;

@Bean
public DiscordApiClient createHttpInterface() {
RestClient restClient = RestClient.builder().baseUrl(discordProperties.webhookUrl()).build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(DiscordApiClient.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.daobe.user.application;

import com.example.daobe.user.domain.event.UserInquiriesEvent;

public interface UserExternalEventAlert {

void execute(UserInquiriesEvent event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.daobe.user.application;

import com.example.daobe.user.domain.event.UserInquiriesEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Service
@RequiredArgsConstructor
public class UserExternalEventListener {

private final UserExternalEventAlert userExternalEventAlert;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void listenInquiriesEvent(UserInquiriesEvent event) {
userExternalEventAlert.execute(event);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/example/daobe/user/application/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import com.example.daobe.user.application.dto.UpdateProfileRequestDto;
import com.example.daobe.user.application.dto.UpdateProfileResponseDto;
import com.example.daobe.user.application.dto.UserInfoResponseDto;
import com.example.daobe.user.application.dto.UserInquiriesRequestDto;
import com.example.daobe.user.application.dto.UserPokeRequestDto;
import com.example.daobe.user.application.dto.UserWithdrawRequestDto;
import com.example.daobe.user.domain.User;
import com.example.daobe.user.domain.event.UserCreateEvent;
import com.example.daobe.user.domain.event.UserInquiriesEvent;
import com.example.daobe.user.domain.event.UserPokeEvent;
import com.example.daobe.user.domain.event.UserUpdateEvent;
import com.example.daobe.user.domain.repository.UserRepository;
Expand All @@ -29,6 +31,8 @@
@RequiredArgsConstructor
public class UserService {

// TODO: 리팩토링

private static final int DEFAULT_VIEW_LIMIT_SIZE = 10;
private static final int DEFAULT_EXECUTE_LIMIT_SIZE = DEFAULT_VIEW_LIMIT_SIZE + 1;

Expand Down Expand Up @@ -108,6 +112,18 @@ public void withdraw(Long userId, UserWithdrawRequestDto request) {
userRepository.save(findUser);
}

public void inquiries(Long userId, UserInquiriesRequestDto request) {
User findUser = userRepository.findById(userId)
.orElseThrow(() -> new UserException(NOT_EXIST_USER));
UserInquiriesEvent event = new UserInquiriesEvent(
userId,
findUser.getNickname(),
request.email(),
request.contents()
);
eventPublisher.publishEvent(event);
}

// External Service
public User getUserById(Long userId) {
return userRepository.findById(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.daobe.user.application.dto;

public record UserInquiriesRequestDto(
String email,
String contents
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.daobe.user.domain.event;

public record UserInquiriesEvent(
Long userId,
String nickname,
String email,
String contents
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.daobe.user.infrastructure.discord;

import com.example.daobe.user.infrastructure.discord.dto.DiscordAlertPayloadDto;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.PostExchange;

public interface DiscordApiClient {

@PostExchange
void execute(@RequestBody DiscordAlertPayloadDto payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.daobe.user.infrastructure.discord;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "discord")
public record DiscordProperties(
String webhookUrl,
String messageFormat
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.daobe.user.infrastructure.discord;

import com.example.daobe.user.application.UserExternalEventAlert;
import com.example.daobe.user.domain.event.UserInquiriesEvent;
import com.example.daobe.user.infrastructure.discord.dto.DiscordAlertPayloadDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class DiscordUserExternalEventAlert implements UserExternalEventAlert {

private static final String DISCORD_MESSAGE_TITLE = "💬 1:1 문의하기";
private static final String DISCORD_MESSAGE_CONTENTS = "## 🚀 새로운 문의가 도착했습니다!";

private final DiscordApiClient discordApiClient;
private final DiscordProperties discordProperties;

@Override
public void execute(UserInquiriesEvent event) {
String description = String.format(
discordProperties.messageFormat(),
event.userId(),
event.nickname(),
event.email(),
event.contents()
);
DiscordAlertPayloadDto payload = DiscordAlertPayloadDto.of(
DISCORD_MESSAGE_CONTENTS,
DISCORD_MESSAGE_TITLE,
description
);
discordApiClient.execute(payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.daobe.user.infrastructure.discord.dto;

import java.util.List;

public record DiscordAlertPayloadDto(
String content,
List<Embeds> embeds
) {

public static DiscordAlertPayloadDto of(String content, String title, String description) {
return new DiscordAlertPayloadDto(content, List.of(new Embeds(title, description)));
}

// Nested
public record Embeds(
String title,
String description
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.example.daobe.user.application.dto.UpdateProfileRequestDto;
import com.example.daobe.user.application.dto.UpdateProfileResponseDto;
import com.example.daobe.user.application.dto.UserInfoResponseDto;
import com.example.daobe.user.application.dto.UserInquiriesRequestDto;
import com.example.daobe.user.application.dto.UserPokeRequestDto;
import com.example.daobe.user.application.dto.UserWithdrawRequestDto;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -102,4 +103,17 @@ public ResponseEntity<ApiResponse<Void>> poke(
null
));
}

@RateLimited(name = "userInquiries", capacity = 1, refillSeconds = 30)
@PostMapping("/inquiries")
public ResponseEntity<ApiResponse<Void>> inquiries(
@AuthenticationPrincipal Long userId,
@RequestBody UserInquiriesRequestDto request
) {
userService.inquiries(userId, request);
return ResponseEntity.ok(new ApiResponse<>(
"INQUIRIES_SUCCESS",
null
));
}
}
31 changes: 26 additions & 5 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
management:
endpoints:
web:
exposure:
include: "prometheus,health"
server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 5s

jackson:
property-naming-strategy: SNAKE_CASE

Expand Down Expand Up @@ -83,6 +83,27 @@ spring:
password: actuator
role: ACTUATOR

management:
endpoints:
web:
exposure:
include: "prometheus,health"

discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
message-format: |
### 👥 사용자 정보
식별자 : `%s`
닉네임 : `%s`
이메일 : `%s`
### 🖋 문의 내용
```
%s
```
security:
jwt:
secret-key: afdakfjlkasjfaioefjklasfmakl;fjaklfmakl;dfaklfadsflka
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/config

0 comments on commit 149b75f

Please sign in to comment.