Compare commits
1 Commits
main
...
feature-mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c27bda82 |
@@ -8,15 +8,6 @@ charset = utf-8
|
||||
# [newline-eof]
|
||||
insert_final_newline = true
|
||||
|
||||
# Ignore
|
||||
[*.log]
|
||||
charset = unset
|
||||
end_of_line = unset
|
||||
insert_final_newline = unset
|
||||
trim_trailing_whitespace = unset
|
||||
indent_style = unset
|
||||
indent_size = unset
|
||||
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -35,8 +35,3 @@ out/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
/logs
|
||||
/logs/*.log
|
||||
|
||||
/src/main/resources/application-prod.yml
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
# YouAndMe
|
||||
지역 사람들과 공감할 수 있는 지도 기반 블로깅 서비스
|
||||

|
||||
|
||||
/
|
||||
## ✨프로젝트 구조
|
||||
(2021.11.01 수정)
|
||||

|
||||
|
||||
74
build.gradle
74
build.gradle
@@ -1,8 +1,7 @@
|
||||
import com.github.spotbugs.snom.SpotBugsTask
|
||||
|
||||
plugins {
|
||||
id 'jacoco'
|
||||
id 'org.springframework.boot' version '2.5.3'
|
||||
id 'org.springframework.boot' version '2.5.4'
|
||||
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
|
||||
id 'org.asciidoctor.convert' version '1.5.8'
|
||||
id 'org.ec4j.editorconfig' version '0.0.3'
|
||||
@@ -19,9 +18,6 @@ configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
all {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -42,92 +38,32 @@ dependencies {
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
|
||||
testImplementation 'com.h2database:h2'
|
||||
runtimeOnly 'mysql:mysql-connector-java'
|
||||
|
||||
implementation 'com.lmax:disruptor:3.4.4'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-mail'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
||||
implementation 'org.springframework.security:spring-security-crypto:5.5.2'
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'de.codecentric:spring-boot-admin-starter-client:2.4.3'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
|
||||
implementation 'it.ozimov:embedded-redis:0.7.2'
|
||||
testImplementation 'it.ozimov:embedded-redis:0.7.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
implementation 'org.springframework.session:spring-session-data-redis'
|
||||
|
||||
testImplementation 'io.github.javaunit:autoparams:0.3.0'
|
||||
testImplementation 'io.github.javaunit:autoparams:0.2.12'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:0.20.1'
|
||||
testImplementation 'org.mockito:mockito-inline:3.9.0'
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
}
|
||||
|
||||
test {
|
||||
outputs.dir snippetsDir
|
||||
useJUnitPlatform()
|
||||
|
||||
finalizedBy('jacocoTestReport')
|
||||
}
|
||||
|
||||
editorconfig {
|
||||
excludes = ["build"]
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = '0.8.7'
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
reports {
|
||||
xml.enabled true
|
||||
html.enabled true
|
||||
csv.enabled false
|
||||
}
|
||||
finalizedBy 'jacocoTestCoverageVerification'
|
||||
|
||||
}
|
||||
|
||||
jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
enabled = true
|
||||
element = 'CLASS'
|
||||
excludes = [
|
||||
"*.YouAndMeApplication",
|
||||
"*.*Request",
|
||||
"*.*Response",
|
||||
"*.*Repository",
|
||||
"*.*Reader",
|
||||
"*.Role",
|
||||
"*.configuration.*",
|
||||
"*.SmtpMail*",
|
||||
"*.RoleTypeHandler",
|
||||
]
|
||||
|
||||
limit {
|
||||
counter = 'BRANCH'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.80
|
||||
}
|
||||
|
||||
limit {
|
||||
counter = 'INSTRUCTION'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check.dependsOn editorconfigCheck
|
||||
|
||||
checkstyle {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
lombok.addLombokGeneratedAnnotation = true
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yam.app;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public final class ActuatorApi {
|
||||
|
||||
@GetMapping("/")
|
||||
public void hello() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
package com.yam.app.account.application;
|
||||
|
||||
import com.yam.app.account.domain.AccountNotFoundException;
|
||||
import com.yam.app.account.domain.AccountReader;
|
||||
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
|
||||
import com.yam.app.account.domain.LoginAccountProcessor;
|
||||
import com.yam.app.account.domain.RegisterAccountConfirmEvent;
|
||||
import com.yam.app.account.domain.RegisterAccountEvent;
|
||||
import com.yam.app.account.domain.RegisterAccountProcessor;
|
||||
import com.yam.app.account.domain.UpdateAccountEvent;
|
||||
import com.yam.app.account.domain.UpdateAccountProcessor;
|
||||
import com.yam.app.account.presentation.AccountResponse;
|
||||
import com.yam.app.account.presentation.ConfirmRegisterAccountCommand;
|
||||
import com.yam.app.account.presentation.LoginAccountCommand;
|
||||
import com.yam.app.account.presentation.RegisterAccountCommand;
|
||||
import com.yam.app.account.presentation.UpdateAccountCommand;
|
||||
import com.yam.app.common.Authentication;
|
||||
import com.yam.app.account.presentation.RegisterAccountRequest;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -22,59 +11,22 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class AccountFacade {
|
||||
|
||||
private final RegisterAccountProcessor registerProcessor;
|
||||
private final RegisterAccountProcessor processor;
|
||||
private final AccountTranslator translator;
|
||||
private final ApplicationEventPublisher publisher;
|
||||
private final ConfirmRegisterAccountProcessor confirmRegisterProcessor;
|
||||
private final LoginAccountProcessor loginProcessor;
|
||||
private final AccountReader accountReader;
|
||||
private final UpdateAccountProcessor updateProcessor;
|
||||
|
||||
public AccountFacade(RegisterAccountProcessor registerProcessor,
|
||||
ApplicationEventPublisher publisher,
|
||||
ConfirmRegisterAccountProcessor confirmRegisterProcessor,
|
||||
LoginAccountProcessor loginProcessor, AccountReader accountReader,
|
||||
UpdateAccountProcessor updateProcessor) {
|
||||
this.registerProcessor = registerProcessor;
|
||||
public AccountFacade(RegisterAccountProcessor processor,
|
||||
AccountTranslator translator,
|
||||
ApplicationEventPublisher publisher) {
|
||||
this.processor = processor;
|
||||
this.translator = translator;
|
||||
this.publisher = publisher;
|
||||
this.confirmRegisterProcessor = confirmRegisterProcessor;
|
||||
this.loginProcessor = loginProcessor;
|
||||
this.accountReader = accountReader;
|
||||
this.updateProcessor = updateProcessor;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void register(RegisterAccountCommand command) {
|
||||
registerProcessor.register(command.getEmail(), command.getPassword());
|
||||
var entity = accountReader.findByEmail(command.getEmail())
|
||||
.orElseThrow(() -> new AccountNotFoundException(command.getEmail()));
|
||||
public AccountResponse register(RegisterAccountRequest request) {
|
||||
var entity = processor.process(translator.toCommand(request));
|
||||
publisher.publishEvent(new RegisterAccountEvent(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void registerConfirm(ConfirmRegisterAccountCommand command) {
|
||||
confirmRegisterProcessor.registerConfirm(command.getToken(), command.getEmail());
|
||||
publisher.publishEvent(new RegisterAccountConfirmEvent(command.getEmail()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public void login(LoginAccountCommand command) {
|
||||
loginProcessor.login(command.getEmail(), command.getPassword());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public AccountResponse findInfo(Authentication authentication) {
|
||||
var memberAccount = accountReader.findByEmailAndMemberId(
|
||||
authentication.getCredentials(), authentication.getMemberId());
|
||||
return new AccountResponse(memberAccount.getId(), memberAccount.getEmail(),
|
||||
memberAccount.getNickname(), memberAccount.getImage());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Authentication authentication, UpdateAccountCommand command) {
|
||||
updateProcessor.update(authentication.getCredentials(), command.getPassword());
|
||||
publisher.publishEvent(new UpdateAccountEvent(
|
||||
authentication.getMemberId(),
|
||||
command.getNickname(),
|
||||
command.getImage()));
|
||||
return translator.toResponse(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yam.app.account.application;
|
||||
|
||||
import com.yam.app.account.domain.Account;
|
||||
import com.yam.app.account.presentation.AccountResponse;
|
||||
import com.yam.app.account.presentation.RegisterAccountRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
final class AccountTranslator {
|
||||
|
||||
public RegisterAccountCommand toCommand(RegisterAccountRequest request) {
|
||||
return new RegisterAccountCommand(request.getEmail(), request.getNickname(),
|
||||
request.getPassword());
|
||||
}
|
||||
|
||||
public AccountResponse toResponse(Account entity) {
|
||||
return new AccountResponse(entity.getId(), entity.getEmail(),
|
||||
entity.getNickname());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yam.app.account.application;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class RegisterAccountCommand {
|
||||
|
||||
private final String email;
|
||||
private final String nickname;
|
||||
private final String password;
|
||||
|
||||
public RegisterAccountCommand(String email, String nickname, String password) {
|
||||
this.email = email;
|
||||
this.nickname = nickname;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import com.yam.app.common.EntityStatus;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString(exclude = "password")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class Account implements Serializable {
|
||||
public final class Account {
|
||||
|
||||
private Long id;
|
||||
private Long memberId;
|
||||
private String email;
|
||||
private String nickname;
|
||||
private String password;
|
||||
private String emailCheckToken;
|
||||
private LocalDateTime emailCheckTokenGeneratedAt;
|
||||
@@ -25,17 +20,15 @@ public final class Account implements Serializable {
|
||||
private LocalDateTime lastModifiedAt;
|
||||
private LocalDateTime withdrawalAt;
|
||||
private boolean withdraw = false;
|
||||
private Role role;
|
||||
private EntityStatus status = EntityStatus.ALIVE;
|
||||
|
||||
private Account(String email, String password) {
|
||||
private Account(String email, String nickname, String password) {
|
||||
this.email = email;
|
||||
this.nickname = nickname;
|
||||
this.password = password;
|
||||
this.role = Role.DEFAULT;
|
||||
}
|
||||
|
||||
public static Account of(String email, String password) {
|
||||
Account account = new Account(email, password);
|
||||
public static Account of(String email, String nickname, String password) {
|
||||
Account account = new Account(email, nickname, password);
|
||||
account.generateEmailCheckToken();
|
||||
return account;
|
||||
}
|
||||
@@ -57,12 +50,4 @@ public final class Account implements Serializable {
|
||||
public boolean isValidToken(String token) {
|
||||
return this.emailCheckToken.equals(token);
|
||||
}
|
||||
|
||||
public void addMember(Long memberId) {
|
||||
this.memberId = memberId;
|
||||
}
|
||||
|
||||
public void changePassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import com.yam.app.common.EntityNotFoundException;
|
||||
|
||||
public final class AccountNotFoundException extends EntityNotFoundException {
|
||||
|
||||
public AccountNotFoundException(String email) {
|
||||
super("Account could not be found, (email : %s)", email);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface AccountReader {
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
Optional<Account> findByEmail(String email);
|
||||
|
||||
MemberAccount findByEmailAndMemberId(@Param("email") String email,
|
||||
@Param("memberId") Long memberId);
|
||||
Account findByEmail(String email);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.yam.app.account.domain;
|
||||
|
||||
public interface AccountRepository {
|
||||
|
||||
void save(Account entity);
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
void update(Account entity);
|
||||
boolean existsByNickname(String nickname);
|
||||
|
||||
Account save(Account entity);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
public final class ConfirmRegisterAccountProcessor {
|
||||
|
||||
private final AccountReader accountReader;
|
||||
private final AccountRepository accountRepository;
|
||||
private final TokenVerifier tokenVerifier;
|
||||
|
||||
public ConfirmRegisterAccountProcessor(AccountReader accountReader,
|
||||
AccountRepository accountRepository, TokenVerifier tokenVerifier) {
|
||||
this.accountReader = accountReader;
|
||||
this.accountRepository = accountRepository;
|
||||
this.tokenVerifier = tokenVerifier;
|
||||
}
|
||||
|
||||
public void registerConfirm(String token, String email) {
|
||||
tokenVerifier.verify(token, email);
|
||||
var account = accountReader.findByEmail(email)
|
||||
.orElseThrow(() -> new AccountNotFoundException(email));
|
||||
account.completeRegister();
|
||||
accountRepository.update(account);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class GenerateMemberEvent {
|
||||
|
||||
private final Long memberId;
|
||||
private final String email;
|
||||
|
||||
public GenerateMemberEvent(Long memberId, String email) {
|
||||
this.memberId = memberId;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
public interface LoginAccountProcessor {
|
||||
|
||||
void login(String email, String password);
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class MemberAccount {
|
||||
|
||||
private final Long id;
|
||||
private final String email;
|
||||
private final String nickname;
|
||||
private final String image;
|
||||
|
||||
public MemberAccount(Long id, String email, String nickname, String image) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class RegisterAccountConfirmEvent {
|
||||
|
||||
private final String email;
|
||||
|
||||
public RegisterAccountConfirmEvent(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,29 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import com.yam.app.common.DuplicateValueException;
|
||||
import com.yam.app.account.application.RegisterAccountCommand;
|
||||
|
||||
public final class RegisterAccountProcessor {
|
||||
|
||||
private final AccountRepository accountRepository;
|
||||
private final AccountReader accountReader;
|
||||
private final PasswordEncrypter passwordEncrypter;
|
||||
|
||||
public RegisterAccountProcessor(AccountRepository accountRepository,
|
||||
AccountReader accountReader, PasswordEncrypter passwordEncrypter) {
|
||||
PasswordEncrypter passwordEncrypter) {
|
||||
this.accountRepository = accountRepository;
|
||||
this.accountReader = accountReader;
|
||||
this.passwordEncrypter = passwordEncrypter;
|
||||
}
|
||||
|
||||
public void register(String email, String password) {
|
||||
if (accountReader.existsByEmail(email)) {
|
||||
throw new DuplicateValueException(email);
|
||||
public Account process(RegisterAccountCommand command) {
|
||||
if (accountRepository.existsByEmail(command.getEmail())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (accountRepository.existsByNickname(command.getNickname())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
String encodedPassword = passwordEncrypter.encode(password);
|
||||
String encodedPassword = passwordEncrypter.encode(command.getPassword());
|
||||
|
||||
accountRepository.save(Account.of(email, encodedPassword));
|
||||
return accountRepository.save(
|
||||
Account.of(command.getEmail(), command.getNickname(), encodedPassword));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum Role {
|
||||
DEFAULT, LOCALIZED, ADMIN;
|
||||
|
||||
public static Role findRole(String role) {
|
||||
return Arrays.stream(Role.values())
|
||||
.filter(r -> r.name().equals(role))
|
||||
.findFirst()
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
public final class TokenVerifier {
|
||||
|
||||
private final AccountReader accountReader;
|
||||
|
||||
public TokenVerifier(AccountReader accountReader) {
|
||||
this.accountReader = accountReader;
|
||||
}
|
||||
|
||||
public void verify(String token, String email) {
|
||||
var account = accountReader.findByEmail(email)
|
||||
.orElseThrow(() -> new AccountNotFoundException(email));
|
||||
|
||||
if (!account.isValidToken(token)) {
|
||||
throw new IllegalStateException("Invalid token");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class UpdateAccountEvent {
|
||||
|
||||
private final Long memberId;
|
||||
private final String nickname;
|
||||
private final String image;
|
||||
|
||||
public UpdateAccountEvent(Long memberId, String nickname, String image) {
|
||||
this.memberId = memberId;
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yam.app.account.domain;
|
||||
|
||||
public final class UpdateAccountProcessor {
|
||||
|
||||
private final AccountReader accountReader;
|
||||
private final AccountRepository accountRepository;
|
||||
private final PasswordEncrypter passwordEncrypter;
|
||||
|
||||
public UpdateAccountProcessor(AccountReader accountReader,
|
||||
AccountRepository accountRepository,
|
||||
PasswordEncrypter passwordEncrypter) {
|
||||
this.accountReader = accountReader;
|
||||
this.accountRepository = accountRepository;
|
||||
this.passwordEncrypter = passwordEncrypter;
|
||||
}
|
||||
|
||||
public void update(String email, String password) {
|
||||
var account = accountReader.findByEmail(email)
|
||||
.orElseThrow(() -> new AccountNotFoundException(email));
|
||||
account.changePassword(passwordEncrypter.encode(password));
|
||||
accountRepository.update(account);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.AccountNotFoundException;
|
||||
import com.yam.app.account.domain.AccountReader;
|
||||
import com.yam.app.account.domain.AccountRepository;
|
||||
import com.yam.app.account.domain.GenerateMemberEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AccountEventListener {
|
||||
|
||||
private final AccountReader accountReader;
|
||||
private final AccountRepository accountRepository;
|
||||
|
||||
public AccountEventListener(AccountReader accountReader,
|
||||
AccountRepository accountRepository) {
|
||||
this.accountReader = accountReader;
|
||||
this.accountRepository = accountRepository;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handle(GenerateMemberEvent event) {
|
||||
var account = accountReader.findByEmail(event.getEmail())
|
||||
.orElseThrow(() -> new AccountNotFoundException(event.getEmail()));
|
||||
account.addMember(event.getMemberId());
|
||||
accountRepository.update(account);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
|
||||
import com.yam.app.account.domain.RegisterAccountProcessor;
|
||||
import com.yam.app.account.domain.TokenVerifier;
|
||||
import com.yam.app.account.domain.UpdateAccountProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Import(value = {
|
||||
MybatisAccountRepository.class,
|
||||
DelegatePasswordEncrypter.class,
|
||||
RegisterAccountProcessor.class,
|
||||
TokenVerifier.class,
|
||||
ConfirmRegisterAccountProcessor.class,
|
||||
SessionBasedLoginAccountProcessor.class,
|
||||
UpdateAccountProcessor.class,
|
||||
SessionManager.class
|
||||
})
|
||||
@Configuration
|
||||
public class AccountModuleConfiguration {
|
||||
|
||||
@Bean
|
||||
@Profile(value = {"local", "prod"})
|
||||
public MailDispatcher mailDispatcher(JavaMailSender javaMailSender) {
|
||||
return new SmtpMailDispatcher(javaMailSender);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.Account;
|
||||
import com.yam.app.common.Authentication;
|
||||
|
||||
public final class AccountPrincipal implements Authentication {
|
||||
|
||||
private final Account account;
|
||||
|
||||
public AccountPrincipal(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCredentials() {
|
||||
return account.getEmail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRole() {
|
||||
return account.getRole().name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getMemberId() {
|
||||
return account.getMemberId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.AccountReader;
|
||||
import com.yam.app.account.domain.AccountRepository;
|
||||
import com.yam.app.account.domain.PasswordEncrypter;
|
||||
import com.yam.app.account.domain.RegisterAccountProcessor;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class AppConfiguration {
|
||||
|
||||
@Bean
|
||||
@Profile("prod")
|
||||
public MailDispatcher mailDispatcher(JavaMailSender javaMailSender) {
|
||||
return new SmtpMailDispatcher(javaMailSender);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncrypter passwordEncrypter(PasswordEncoder passwordEncoder) {
|
||||
return new DelegatePasswordEncrypter(passwordEncoder);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AccountRepository accountRepository(SqlSessionTemplate sqlSessionTemplate) {
|
||||
return new MybatisAccountRepository(sqlSessionTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AccountReader accountReader(SqlSessionTemplate sqlSessionTemplate) {
|
||||
return new MybatisAccountRepository(sqlSessionTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RegisterAccountProcessor registerAccountProcessor(AccountRepository accountRepository,
|
||||
PasswordEncrypter passwordEncrypter) {
|
||||
return new RegisterAccountProcessor(accountRepository, passwordEncrypter);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.yam.app.common.configuration;
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.common.AuthenticationPrincipal;
|
||||
import com.yam.app.common.UnauthorizedRequestException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
public final class AuthenticationPrincipalArgumentResolver
|
||||
implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
HttpSession session = ((HttpServletRequest) webRequest.getNativeRequest())
|
||||
.getSession(false);
|
||||
|
||||
if (session == null) {
|
||||
throw new UnauthorizedRequestException("Unauthorized request");
|
||||
}
|
||||
|
||||
var sessionManager = new SessionManager(session);
|
||||
return sessionManager.fetchPrincipal()
|
||||
.orElseThrow(() -> new UnauthorizedRequestException("Failed fetch principal"));
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.RegisterAccountEvent;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
@@ -23,15 +23,14 @@ class MailManager {
|
||||
}
|
||||
|
||||
@Async
|
||||
@TransactionalEventListener
|
||||
@EventListener
|
||||
public void handle(RegisterAccountEvent event) {
|
||||
var newAccount = event.getAccount();
|
||||
var context = new Context();
|
||||
context.setVariable("link",
|
||||
"/api/accounts/authorize?token=" + newAccount.getEmailCheckToken()
|
||||
"/api/check-email?token=" + newAccount.getEmailCheckToken()
|
||||
+ "&email=" + newAccount.getEmail());
|
||||
var username = newAccount.getEmail().split("@")[0];
|
||||
context.setVariable("username", username);
|
||||
context.setVariable("nickname", newAccount.getNickname());
|
||||
context.setVariable("linkName", "이메일 인증하기");
|
||||
context.setVariable("message", "YouAndMe 서비스를 사용하려면 링크를 클릭하세요.");
|
||||
context.setVariable("host", host);
|
||||
|
||||
@@ -3,52 +3,43 @@ package com.yam.app.account.infrastructure;
|
||||
import com.yam.app.account.domain.Account;
|
||||
import com.yam.app.account.domain.AccountReader;
|
||||
import com.yam.app.account.domain.AccountRepository;
|
||||
import com.yam.app.account.domain.MemberAccount;
|
||||
import java.util.Optional;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
public final class MybatisAccountRepository implements AccountRepository, AccountReader {
|
||||
|
||||
private static final String SAVE_FQCN = "com.yam.app.account.domain.AccountRepository.save";
|
||||
private static final String UPDATE_FQCN = "com.yam.app.account.domain.AccountRepository.update";
|
||||
|
||||
private final SqlSessionTemplate template;
|
||||
|
||||
private static final String COMMAND_NAMESPACE = "com.yam.app.account.domain.AccountRepository.";
|
||||
private static final String READER_NAMESPACE = "com.yam.app.account.domain.AccountReader.";
|
||||
|
||||
public MybatisAccountRepository(SqlSessionTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
return template.getMapper(AccountReader.class).existsByEmail(email);
|
||||
int result = template.selectOne(COMMAND_NAMESPACE + "existsByEmail", email);
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Account entity) {
|
||||
int result = template.update(UPDATE_FQCN, entity);
|
||||
public boolean existsByNickname(String nickname) {
|
||||
int result = template.selectOne(COMMAND_NAMESPACE + "existsByNickname", nickname);
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account save(Account entity) {
|
||||
int result = template.insert(COMMAND_NAMESPACE + "save", entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were updated than expected. : %s", entity));
|
||||
throw new RuntimeException(
|
||||
String.format("There was a problem saving the object : %s", entity));
|
||||
}
|
||||
return findByEmail(entity.getEmail());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Account entity) {
|
||||
int result = template.insert(SAVE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were saved than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Account> findByEmail(String email) {
|
||||
return template.getMapper(AccountReader.class).findByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MemberAccount findByEmailAndMemberId(String email,
|
||||
Long memberId) {
|
||||
return template.getMapper(AccountReader.class).findByEmailAndMemberId(email, memberId);
|
||||
public Account findByEmail(String email) {
|
||||
return template.selectOne(READER_NAMESPACE + "findByEmail", email);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.Role;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
|
||||
@MappedTypes(RoleTypeHandler.class)
|
||||
public final class RoleTypeHandler implements TypeHandler<Role> {
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i,
|
||||
Role parameter, JdbcType jdbcType) throws SQLException {
|
||||
ps.setString(i, parameter.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Role getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return Role.findRole(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Role getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return Role.findRole(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Role getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return Role.findRole(cs.getString(columnIndex));
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.common.UnauthorizedRequestException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
public final class SessionAuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
var session = request.getSession(false);
|
||||
if (session == null) {
|
||||
throw new UnauthorizedRequestException("Unauthorized request");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.account.domain.AccountNotFoundException;
|
||||
import com.yam.app.account.domain.AccountReader;
|
||||
import com.yam.app.account.domain.LoginAccountProcessor;
|
||||
import com.yam.app.account.domain.PasswordEncrypter;
|
||||
|
||||
public final class SessionBasedLoginAccountProcessor implements LoginAccountProcessor {
|
||||
|
||||
private final AccountReader accountReader;
|
||||
private final PasswordEncrypter passwordEncrypter;
|
||||
private final SessionManager sessionManager;
|
||||
|
||||
public SessionBasedLoginAccountProcessor(AccountReader accountReader,
|
||||
PasswordEncrypter passwordEncrypter,
|
||||
SessionManager sessionManager) {
|
||||
this.accountReader = accountReader;
|
||||
this.passwordEncrypter = passwordEncrypter;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void login(String email, String password) {
|
||||
var account = accountReader.findByEmail(email)
|
||||
.orElseThrow(() -> new AccountNotFoundException(email));
|
||||
|
||||
if (!account.isEmailVerified()) {
|
||||
throw new IllegalStateException("Email not verified");
|
||||
}
|
||||
|
||||
if (!passwordEncrypter.matches(password, account.getPassword())) {
|
||||
throw new IllegalStateException("Password mismatched");
|
||||
}
|
||||
|
||||
sessionManager.setPrincipal(new AccountPrincipal(account));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import com.yam.app.common.Authentication;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public final class SessionManager {
|
||||
|
||||
private static final String LOGIN_ACCOUNT = "LOGIN_ACCOUNT";
|
||||
|
||||
private final HttpSession httpSession;
|
||||
|
||||
public SessionManager(HttpSession httpSession) {
|
||||
this.httpSession = httpSession;
|
||||
}
|
||||
|
||||
public void setPrincipal(Authentication principal) {
|
||||
this.httpSession.setAttribute(LOGIN_ACCOUNT, principal);
|
||||
}
|
||||
|
||||
public Optional<Authentication> fetchPrincipal() {
|
||||
return Optional.ofNullable((Authentication) httpSession.getAttribute(LOGIN_ACCOUNT));
|
||||
}
|
||||
|
||||
public void removePrincipal() {
|
||||
this.httpSession.removeAttribute(LOGIN_ACCOUNT);
|
||||
this.httpSession.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.yam.app.account.infrastructure;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/accounts/login",
|
||||
"/api/accounts/authorize",
|
||||
"/api/accounts",
|
||||
"/api/articles/all",
|
||||
"/api/articles/{articleId:[0-9]+}",
|
||||
"/api/comments/{articleId:[0-9]+}",
|
||||
"/actuator/**"
|
||||
};
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
resolvers.add(new AuthenticationPrincipalArgumentResolver());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SessionAuthInterceptor())
|
||||
.addPathPatterns("/api/**")
|
||||
.excludePathPatterns(EXCLUDE_PATHS);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import com.yam.app.account.application.AccountFacade;
|
||||
import com.yam.app.account.infrastructure.SessionManager;
|
||||
import com.yam.app.common.Authentication;
|
||||
import com.yam.app.common.AuthenticationPrincipal;
|
||||
import java.net.URI;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class AccountCommandApi {
|
||||
|
||||
private final AccountFacade accountFacade;
|
||||
|
||||
public AccountCommandApi(AccountFacade accountFacade) {
|
||||
this.accountFacade = accountFacade;
|
||||
}
|
||||
|
||||
@PostMapping("/api/accounts")
|
||||
public ResponseEntity<Void> register(
|
||||
@RequestBody @Valid RegisterAccountCommand command) {
|
||||
accountFacade.register(command);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시로 "http://localhost:3000/login"로 리다이렉트 되도록 설정.
|
||||
*/
|
||||
@GetMapping(value = "/api/accounts/authorize",
|
||||
produces = MediaType.ALL_VALUE,
|
||||
consumes = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Void> registerConfirm(
|
||||
@ModelAttribute @Valid ConfirmRegisterAccountCommand command) throws Exception {
|
||||
accountFacade.registerConfirm(command);
|
||||
|
||||
var uri = new URI("http://localhost:3000/login");
|
||||
var header = new HttpHeaders();
|
||||
header.setLocation(uri);
|
||||
return new ResponseEntity<>(header, HttpStatus.SEE_OTHER);
|
||||
}
|
||||
|
||||
@PostMapping("/api/accounts/login")
|
||||
public ResponseEntity<Void> login(
|
||||
@RequestBody @Valid LoginAccountCommand request) {
|
||||
accountFacade.login(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/api/accounts/logout")
|
||||
public ResponseEntity<Void> logout(HttpSession httpSession) {
|
||||
var session = new SessionManager(httpSession);
|
||||
session.removePrincipal();
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PatchMapping("/api/accounts/update")
|
||||
public ResponseEntity<Void> update(
|
||||
@RequestBody @Valid UpdateAccountCommand command,
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
accountFacade.update(authentication, command);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,10 @@ public final class AccountResponse {
|
||||
private final Long id;
|
||||
private final String email;
|
||||
private final String nickname;
|
||||
private final String image;
|
||||
|
||||
public AccountResponse(Long id, String email, String nickname, String image) {
|
||||
public AccountResponse(Long id, String email, String nickname) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class ConfirmRegisterAccountCommand {
|
||||
|
||||
@NotBlank
|
||||
private String token;
|
||||
|
||||
@Email
|
||||
@NotBlank
|
||||
private String email;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class LoginAccountCommand {
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$",
|
||||
message = "Please enter the password in English, numbers, "
|
||||
+ "and special characters within 8-12 digits.")
|
||||
private String password;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import com.yam.app.account.application.AccountFacade;
|
||||
import com.yam.app.common.ApiResult;
|
||||
import com.yam.app.common.Authentication;
|
||||
import com.yam.app.common.AuthenticationPrincipal;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -15,19 +14,17 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class AccountQueryApi {
|
||||
public final class RegisterAccountApi {
|
||||
|
||||
private final AccountFacade accountFacade;
|
||||
|
||||
public AccountQueryApi(AccountFacade accountFacade) {
|
||||
public RegisterAccountApi(AccountFacade accountFacade) {
|
||||
this.accountFacade = accountFacade;
|
||||
}
|
||||
|
||||
@GetMapping("/api/accounts/me")
|
||||
public ResponseEntity<ApiResult<?>> findInfo(
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
return ResponseEntity.ok(
|
||||
ApiResult.success(accountFacade.findInfo(authentication)));
|
||||
@PostMapping("/api/accounts")
|
||||
public ResponseEntity<AccountResponse> register(
|
||||
@RequestBody @Valid RegisterAccountRequest request) {
|
||||
return ResponseEntity.ok(accountFacade.register(request));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class RegisterAccountCommand {
|
||||
|
||||
@Email
|
||||
@NotBlank
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$",
|
||||
message = "Please enter the password in English, numbers, "
|
||||
+ "and special characters within 8-12 digits.")
|
||||
private String password;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class RegisterAccountRequest {
|
||||
|
||||
@Email
|
||||
@NotBlank
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
private String nickname;
|
||||
|
||||
@Size(min = 8, max = 12, message = "비밀번호는 최소 8자, 최대 12자까지 허용됩니다.")
|
||||
@NotBlank
|
||||
private String password;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.yam.app.account.presentation;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class UpdateAccountCommand {
|
||||
|
||||
@NotBlank
|
||||
private String nickname;
|
||||
|
||||
@NotBlank
|
||||
private String image;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$",
|
||||
message = "Please enter the password in English, numbers, "
|
||||
+ "and special characters within 8-12 digits.")
|
||||
private String password;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.yam.app.adapter;
|
||||
|
||||
import com.yam.app.account.domain.RegisterAccountConfirmEvent;
|
||||
import com.yam.app.account.domain.UpdateAccountEvent;
|
||||
import com.yam.app.member.domain.GenerateMemberEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
class DomainEventTranslator {
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
public DomainEventTranslator(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void translate(RegisterAccountConfirmEvent event) {
|
||||
publisher.publishEvent(
|
||||
new com.yam.app.member.domain.RegisterAccountConfirmEvent(event.getEmail()));
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void translate(GenerateMemberEvent event) {
|
||||
publisher.publishEvent(
|
||||
new com.yam.app.account.domain.GenerateMemberEvent(event.getMemberId(),
|
||||
event.getEmail()));
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void translate(UpdateAccountEvent event) {
|
||||
publisher.publishEvent(
|
||||
new com.yam.app.member.domain.UpdateAccountEvent(
|
||||
event.getMemberId(), event.getNickname(), event.getImage()));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.yam.app.article.application;
|
||||
|
||||
import com.yam.app.article.domain.ArticleNotFoundException;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.article.domain.WriteArticleProcessor;
|
||||
import com.yam.app.article.presentation.ArticlePreviewResponse;
|
||||
import com.yam.app.article.presentation.ArticleResponse;
|
||||
import com.yam.app.article.presentation.TagResponse;
|
||||
import com.yam.app.article.presentation.WriteArticleCommand;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ArticleFacade {
|
||||
|
||||
private final WriteArticleProcessor writeArticleProcessor;
|
||||
private final ArticleReader articleReader;
|
||||
|
||||
public ArticleFacade(WriteArticleProcessor writeArticleProcessor,
|
||||
ArticleReader articleReader) {
|
||||
this.writeArticleProcessor = writeArticleProcessor;
|
||||
this.articleReader = articleReader;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void write(Long memberId, WriteArticleCommand command) {
|
||||
writeArticleProcessor.write(memberId,
|
||||
command.getTitle(), command.getContent(),
|
||||
command.getImage(), command.getTags());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ArticlePreviewResponse> findAll(Long articleId, int pageSize) {
|
||||
return articleReader.findAll(articleId, pageSize)
|
||||
.stream()
|
||||
.map(dto -> new ArticlePreviewResponse(dto.getId(), dto.getAuthorId(), dto.getTitle(),
|
||||
dto.getNickname(), dto.getImage(), dto.getCreatedAt(), dto.getModifiedAt(),
|
||||
dto.getStatus())
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ArticleResponse findById(Long articleId) {
|
||||
var article = articleReader.findById(articleId).orElseThrow(
|
||||
() -> new ArticleNotFoundException(articleId));
|
||||
|
||||
return ArticleResponse.builder()
|
||||
.id(article.getId())
|
||||
.authorId(article.getAuthorId())
|
||||
.title(article.getTitle())
|
||||
.content(article.getContent())
|
||||
.image(article.getImage())
|
||||
.createdAt(article.getCreatedAt())
|
||||
.modifiedAt(article.getModifiedAt())
|
||||
.tags(
|
||||
article.getTags().stream()
|
||||
.map(a -> TagResponse.of(a.getTag().getId(), a.getTag().getName()))
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
.build();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import static java.time.LocalDateTime.now;
|
||||
|
||||
import com.yam.app.common.EntityStatus;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public final class Article {
|
||||
|
||||
private Long id;
|
||||
private Long authorId;
|
||||
private String title;
|
||||
private String content;
|
||||
private String image;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
private EntityStatus status = EntityStatus.ALIVE;
|
||||
private List<ArticleTag> tags = new ArrayList<>();
|
||||
|
||||
private Article(Long authorId, String title, String content, String image,
|
||||
LocalDateTime createdAt, LocalDateTime modifiedAt) {
|
||||
this.authorId = authorId;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.image = image;
|
||||
this.createdAt = createdAt;
|
||||
this.modifiedAt = modifiedAt;
|
||||
}
|
||||
|
||||
void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public static Article write(Long authorId, String title, String content,
|
||||
String image) {
|
||||
return new Article(authorId, title, content, image, now(), now());
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class ArticleDto {
|
||||
|
||||
private Long id;
|
||||
private Long authorId;
|
||||
private String title;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
private String nickname;
|
||||
private String image;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import com.yam.app.common.EntityNotFoundException;
|
||||
|
||||
public final class ArticleNotFoundException extends EntityNotFoundException {
|
||||
|
||||
public ArticleNotFoundException(Long id) {
|
||||
super("Article could not be found, (id : %s)", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface ArticleReader {
|
||||
|
||||
Article findByTitle(String title);
|
||||
|
||||
Optional<Article> findById(Long articleId);
|
||||
|
||||
boolean existsById(Long articleId);
|
||||
|
||||
List<ArticleDto> findAll(@Param("articleId") Long articleId,
|
||||
@Param("pageSize") int pageSize);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
public interface ArticleRepository {
|
||||
|
||||
void save(Article entity);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class ArticleTag {
|
||||
|
||||
private Long id;
|
||||
private Long articleId;
|
||||
private Tag tag;
|
||||
|
||||
public ArticleTag(Long articleId, Tag tag) {
|
||||
this.articleId = articleId;
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
public interface ArticleTagRepository {
|
||||
|
||||
void save(ArticleTag entity);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class Tag {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Tag(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
public interface TagRepository {
|
||||
|
||||
void save(Tag entity);
|
||||
|
||||
Tag findByName(String name);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class WriteArticleProcessor {
|
||||
|
||||
private final ArticleRepository articleRepository;
|
||||
private final ArticleReader articleReader;
|
||||
private final TagRepository tagRepository;
|
||||
private final ArticleTagRepository articleTagRepository;
|
||||
|
||||
public WriteArticleProcessor(ArticleRepository articleRepository,
|
||||
ArticleReader articleReader, TagRepository tagRepository,
|
||||
ArticleTagRepository articleTagRepository) {
|
||||
this.articleRepository = articleRepository;
|
||||
this.articleReader = articleReader;
|
||||
this.tagRepository = tagRepository;
|
||||
this.articleTagRepository = articleTagRepository;
|
||||
}
|
||||
|
||||
public void write(Long authorId, String title, String content,
|
||||
String image, List<String> rawTags) {
|
||||
if (articleReader.findByTitle(title) != null) {
|
||||
throw new IllegalStateException("Duplicated title");
|
||||
}
|
||||
articleRepository.save(Article.write(authorId, title, content, image));
|
||||
var newArticle = articleReader.findByTitle(title);
|
||||
|
||||
for (String rawTag : rawTags) {
|
||||
Tag tag = Optional.ofNullable(tagRepository.findByName(rawTag))
|
||||
.orElseGet(() -> {
|
||||
tagRepository.save(new Tag(rawTag));
|
||||
return tagRepository.findByName(rawTag);
|
||||
});
|
||||
articleTagRepository.save(new ArticleTag(newArticle.getId(), tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.article.domain.ArticleRepository;
|
||||
import com.yam.app.article.domain.ArticleTagRepository;
|
||||
import com.yam.app.article.domain.TagRepository;
|
||||
import com.yam.app.article.domain.WriteArticleProcessor;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ArticleModuleConfiguration {
|
||||
|
||||
@Bean
|
||||
public ArticleReader articleReader(SqlSessionTemplate template) {
|
||||
return new MybatisArticleRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ArticleRepository articleRepository(SqlSessionTemplate template) {
|
||||
return new MybatisArticleRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ArticleTagRepository articleTagRepository(SqlSessionTemplate template) {
|
||||
return new MybatisArticleTagRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TagRepository tagRepository(SqlSessionTemplate template) {
|
||||
return new MybatisTagRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WriteArticleProcessor writeArticleProcessor(
|
||||
TagRepository tagRepository,
|
||||
ArticleRepository articleRepository,
|
||||
ArticleReader articleReader,
|
||||
ArticleTagRepository articleTagRepository
|
||||
) {
|
||||
return new WriteArticleProcessor(articleRepository, articleReader, tagRepository,
|
||||
articleTagRepository);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.Article;
|
||||
import com.yam.app.article.domain.ArticleDto;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.article.domain.ArticleRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
public final class MybatisArticleRepository implements ArticleReader, ArticleRepository {
|
||||
|
||||
private static final String SAVE_FQCN = "com.yam.app.article.domain.ArticleRepository.save";
|
||||
|
||||
private final SqlSessionTemplate template;
|
||||
|
||||
public MybatisArticleRepository(SqlSessionTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Article entity) {
|
||||
int result = template.insert(SAVE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were saved than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Article findByTitle(String title) {
|
||||
return template.getMapper(ArticleReader.class).findByTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Article> findById(Long articleId) {
|
||||
return template.getMapper(ArticleReader.class).findById(articleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(Long articleId) {
|
||||
return template.getMapper(ArticleReader.class).existsById(articleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArticleDto> findAll(Long articleId, int pageSize) {
|
||||
return template.getMapper(ArticleReader.class).findAll(articleId, pageSize);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.ArticleTag;
|
||||
import com.yam.app.article.domain.ArticleTagRepository;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
public final class MybatisArticleTagRepository implements ArticleTagRepository {
|
||||
|
||||
private static final String SAVE_FQCN = "com.yam.app.article.domain.ArticleTagRepository.save";
|
||||
|
||||
private final SqlSessionTemplate template;
|
||||
|
||||
public MybatisArticleTagRepository(SqlSessionTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ArticleTag entity) {
|
||||
int result = template.insert(SAVE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were saved than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.Tag;
|
||||
import com.yam.app.article.domain.TagRepository;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
public final class MybatisTagRepository implements TagRepository {
|
||||
|
||||
private static final String SAVE_FQCN = "com.yam.app.article.domain.TagRepository.save";
|
||||
|
||||
private final SqlSessionTemplate template;
|
||||
|
||||
public MybatisTagRepository(SqlSessionTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Tag entity) {
|
||||
int result = template.insert(SAVE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were saved than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tag findByName(String name) {
|
||||
return template.getMapper(TagRepository.class).findByName(name);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import com.yam.app.article.application.ArticleFacade;
|
||||
import com.yam.app.common.Authentication;
|
||||
import com.yam.app.common.AuthenticationPrincipal;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class ArticleCommandApi {
|
||||
|
||||
private final ArticleFacade articleFacade;
|
||||
|
||||
public ArticleCommandApi(ArticleFacade articleFacade) {
|
||||
this.articleFacade = articleFacade;
|
||||
}
|
||||
|
||||
@PostMapping("/api/articles/write")
|
||||
public ResponseEntity<Void> writeArticle(
|
||||
@RequestBody @Valid WriteArticleCommand command,
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
articleFacade.write(authentication.getMemberId(), command);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).build();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class ArticlePreviewResponse {
|
||||
|
||||
private final Long id;
|
||||
private final Long authorId;
|
||||
private final String title;
|
||||
private final String nickname;
|
||||
private final String memberImage;
|
||||
private final LocalDateTime createdAt;
|
||||
private final LocalDateTime modifiedAt;
|
||||
private final String status;
|
||||
|
||||
public ArticlePreviewResponse(Long id, Long authorId, String title,
|
||||
String nickname, String memberImage, LocalDateTime createdAt,
|
||||
LocalDateTime modifiedAt, String status) {
|
||||
this.id = id;
|
||||
this.authorId = authorId;
|
||||
this.title = title;
|
||||
this.nickname = nickname;
|
||||
this.memberImage = memberImage;
|
||||
this.createdAt = createdAt;
|
||||
this.modifiedAt = modifiedAt;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import com.yam.app.article.application.ArticleFacade;
|
||||
import com.yam.app.common.ApiResult;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class ArticleQueryApi {
|
||||
|
||||
private final ArticleFacade articleFacade;
|
||||
|
||||
public ArticleQueryApi(ArticleFacade articleFacade) {
|
||||
this.articleFacade = articleFacade;
|
||||
}
|
||||
|
||||
@GetMapping("/api/articles/all")
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(value = "articleId", defaultValue = "0") Long articleId,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
|
||||
return ResponseEntity.ok(
|
||||
ApiResult.success(articleFacade.findAll(articleId, pageSize)));
|
||||
}
|
||||
|
||||
@GetMapping("/api/articles/{articleId}")
|
||||
public ResponseEntity<ArticleResponse> findArticle(@PathVariable Long articleId) {
|
||||
return ResponseEntity.ok(articleFacade.findById(articleId));
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class ArticleResponse {
|
||||
|
||||
private final Long id;
|
||||
private final Long authorId;
|
||||
private final String title;
|
||||
private final String content;
|
||||
private final String image;
|
||||
private final LocalDateTime createdAt;
|
||||
private final LocalDateTime modifiedAt;
|
||||
private final List<TagResponse> tags;
|
||||
|
||||
private ArticleResponse(Long id, Long authorId, String title, String content, String image,
|
||||
LocalDateTime createdAt, LocalDateTime modifiedAt,
|
||||
List<TagResponse> tags) {
|
||||
this.id = id;
|
||||
this.authorId = authorId;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.image = image;
|
||||
this.createdAt = createdAt;
|
||||
this.modifiedAt = modifiedAt;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class TagResponse {
|
||||
|
||||
private final Long id;
|
||||
private final String name;
|
||||
|
||||
private TagResponse(Long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static TagResponse of(Long id, String name) {
|
||||
return new TagResponse(id, name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class WriteArticleCommand {
|
||||
|
||||
@NotBlank
|
||||
private String title;
|
||||
@NotBlank
|
||||
private String content;
|
||||
@NotBlank
|
||||
private String image;
|
||||
@Size(max = 3)
|
||||
private List<String> tags;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.yam.app.comment.application;
|
||||
|
||||
import com.yam.app.article.domain.ArticleNotFoundException;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.comment.domain.Comment;
|
||||
import com.yam.app.comment.domain.CommentProcessor;
|
||||
import com.yam.app.comment.domain.CommentReader;
|
||||
import com.yam.app.comment.presentation.CommentResponse;
|
||||
import com.yam.app.comment.presentation.CreateCommentCommand;
|
||||
import com.yam.app.comment.presentation.UpdateCommentCommand;
|
||||
import com.yam.app.member.domain.MemberReader;
|
||||
import com.yam.app.member.presentation.MemberResponse;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class CommentFacade {
|
||||
|
||||
private final CommentProcessor commentProcessor;
|
||||
private final ArticleReader articleReader;
|
||||
private final CommentReader commentReader;
|
||||
private final MemberReader memberReader;
|
||||
|
||||
public CommentFacade(CommentProcessor commentProcessor,
|
||||
ArticleReader articleReader, CommentReader commentReader,
|
||||
MemberReader memberReader) {
|
||||
this.commentProcessor = commentProcessor;
|
||||
this.articleReader = articleReader;
|
||||
this.commentReader = commentReader;
|
||||
this.memberReader = memberReader;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(CreateCommentCommand request, Long memberId) {
|
||||
return commentProcessor.create(request.getContent(), request.getArticleId(), memberId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(UpdateCommentCommand request, Long commentId, Long memberId) {
|
||||
commentProcessor.update(request.getContent(), commentId, memberId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long commentId, Long memberId) {
|
||||
commentProcessor.delete(commentId, memberId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CommentResponse> findByArticleId(Long articleId) {
|
||||
if (!articleReader.existsById(articleId)) {
|
||||
throw new ArticleNotFoundException(articleId);
|
||||
}
|
||||
|
||||
return commentReader.findByArticleId(articleId)
|
||||
.stream()
|
||||
.filter(Comment::isAlive)
|
||||
.map(this::toCommentResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CommentResponse toCommentResponse(Comment comment) {
|
||||
var member = memberReader.findById(comment.getMemberId())
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
|
||||
return CommentResponse.builder()
|
||||
.id(comment.getId())
|
||||
.articleId(comment.getArticleId())
|
||||
.content(comment.getContent())
|
||||
.createAt(comment.getCreatedAt())
|
||||
.modifiedAt(comment.getModifiedAt())
|
||||
.author(
|
||||
MemberResponse.builder()
|
||||
.id(member.getId())
|
||||
.image(member.getImage())
|
||||
.nickname(member.getNickname())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.yam.app.comment.domain;
|
||||
|
||||
import static java.time.LocalDateTime.now;
|
||||
|
||||
import com.yam.app.common.EntityStatus;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public final class Comment {
|
||||
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
private EntityStatus status = EntityStatus.ALIVE;
|
||||
private Long articleId;
|
||||
private Long memberId;
|
||||
|
||||
private Comment(String content, LocalDateTime createdAt, LocalDateTime modifiedAt,
|
||||
Long articleId, Long memberId) {
|
||||
this.content = content;
|
||||
this.createdAt = createdAt;
|
||||
this.modifiedAt = modifiedAt;
|
||||
this.articleId = articleId;
|
||||
this.memberId = memberId;
|
||||
}
|
||||
|
||||
public static Comment of(String content, Long articleId, Long memberId) {
|
||||
return new Comment(content, now(), now(), articleId, memberId);
|
||||
}
|
||||
|
||||
public void update(String content) {
|
||||
this.content = content;
|
||||
this.modifiedAt = now();
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
this.status = EntityStatus.DELETED;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return this.status == EntityStatus.ALIVE;
|
||||
}
|
||||
|
||||
public boolean isAuthor(Long memberId) {
|
||||
return this.memberId.equals(memberId);
|
||||
}
|
||||
|
||||
void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.yam.app.comment.domain;
|
||||
|
||||
import com.yam.app.common.EntityNotFoundException;
|
||||
|
||||
public final class CommentNotFoundException extends EntityNotFoundException {
|
||||
|
||||
public CommentNotFoundException(Long id) {
|
||||
super("Comment could not be found, It may have been deleted.(id : %s)", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.yam.app.comment.domain;
|
||||
|
||||
import com.yam.app.article.domain.ArticleNotFoundException;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.common.UnauthorizedRequestException;
|
||||
|
||||
public final class CommentProcessor {
|
||||
|
||||
private final CommentReader commentReader;
|
||||
private final CommentRepository commentRepository;
|
||||
private final ArticleReader articleReader;
|
||||
|
||||
public CommentProcessor(CommentReader commentReader,
|
||||
CommentRepository commentRepository, ArticleReader articleReader) {
|
||||
this.commentReader = commentReader;
|
||||
this.commentRepository = commentRepository;
|
||||
this.articleReader = articleReader;
|
||||
}
|
||||
|
||||
public Long create(String content, Long articleId, Long memberId) {
|
||||
if (!articleReader.existsById(articleId)) {
|
||||
throw new ArticleNotFoundException(articleId);
|
||||
}
|
||||
|
||||
return commentRepository.save(Comment.of(content, articleId, memberId));
|
||||
}
|
||||
|
||||
public void update(String content, Long commentId, Long memberId) {
|
||||
var comment = this.getComment(commentId, memberId);
|
||||
|
||||
if (!comment.isAlive()) {
|
||||
throw new CommentNotFoundException(commentId);
|
||||
}
|
||||
|
||||
comment.update(content);
|
||||
commentRepository.update(comment);
|
||||
}
|
||||
|
||||
public void delete(Long commentId, Long memberId) {
|
||||
var comment = this.getComment(commentId, memberId);
|
||||
|
||||
comment.delete();
|
||||
commentRepository.delete(comment);
|
||||
}
|
||||
|
||||
private Comment getComment(Long commentId, Long memberId) {
|
||||
var comment = commentReader.findById(commentId)
|
||||
.orElseThrow(() -> new CommentNotFoundException(commentId));
|
||||
|
||||
if (!articleReader.existsById(comment.getArticleId())) {
|
||||
throw new ArticleNotFoundException(comment.getArticleId());
|
||||
}
|
||||
|
||||
if (!comment.isAuthor(memberId)) {
|
||||
throw new UnauthorizedRequestException(
|
||||
"Member is not the author of this comment, id: " + memberId);
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yam.app.comment.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CommentReader {
|
||||
|
||||
Optional<Comment> findById(Long commentId);
|
||||
|
||||
List<Comment> findByArticleId(Long articleId);
|
||||
|
||||
boolean existsById(Long commentId);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.yam.app.comment.domain;
|
||||
|
||||
public interface CommentRepository {
|
||||
|
||||
Long save(Comment entity);
|
||||
|
||||
void update(Comment entity);
|
||||
|
||||
void delete(Comment entity);
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.yam.app.comment.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.comment.domain.CommentProcessor;
|
||||
import com.yam.app.comment.domain.CommentReader;
|
||||
import com.yam.app.comment.domain.CommentRepository;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class CommentModuleConfiguration {
|
||||
|
||||
@Bean
|
||||
public CommentReader commentReader(SqlSessionTemplate template) {
|
||||
return new MybatisCommentRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommentRepository commentRepository(SqlSessionTemplate template) {
|
||||
return new MybatisCommentRepository(template);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommentProcessor commentProcessor(CommentReader commentReader,
|
||||
CommentRepository commentRepository, ArticleReader articleReader) {
|
||||
return new CommentProcessor(commentReader, commentRepository, articleReader);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.yam.app.comment.infrastructure;
|
||||
|
||||
import com.yam.app.comment.domain.Comment;
|
||||
import com.yam.app.comment.domain.CommentReader;
|
||||
import com.yam.app.comment.domain.CommentRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
|
||||
public final class MybatisCommentRepository implements CommentReader, CommentRepository {
|
||||
|
||||
private static final String SAVE_FQCN = "com.yam.app.comment.domain.CommentRepository.save";
|
||||
private static final String UPDATE_FQCN = "com.yam.app.comment.domain.CommentRepository.update";
|
||||
private static final String DELETE_FQCN = "com.yam.app.comment.domain.CommentRepository.delete";
|
||||
|
||||
private final SqlSessionTemplate template;
|
||||
|
||||
public MybatisCommentRepository(SqlSessionTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long save(Comment entity) {
|
||||
int result = template.insert(SAVE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were saved than expected. : %s", entity));
|
||||
}
|
||||
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Comment entity) {
|
||||
int result = template.update(UPDATE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were updated than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Comment entity) {
|
||||
int result = template.update(DELETE_FQCN, entity);
|
||||
if (result != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Unintentionally, more records were soft-deleted than expected. : %s", entity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Comment> findById(Long commentId) {
|
||||
return template.getMapper(CommentReader.class).findById(commentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Comment> findByArticleId(Long articleId) {
|
||||
return template.getMapper(CommentReader.class).findByArticleId(articleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(Long commentId) {
|
||||
return template.getMapper(CommentReader.class).existsById(commentId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.yam.app.comment.presentation;
|
||||
|
||||
import com.yam.app.comment.application.CommentFacade;
|
||||
import com.yam.app.common.Authentication;
|
||||
import com.yam.app.common.AuthenticationPrincipal;
|
||||
import java.net.URI;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class CommentCommandApi {
|
||||
|
||||
private final CommentFacade commentFacade;
|
||||
|
||||
public CommentCommandApi(CommentFacade commentFacade) {
|
||||
this.commentFacade = commentFacade;
|
||||
}
|
||||
|
||||
@PostMapping("/api/comments/")
|
||||
public ResponseEntity<Void> createComment(
|
||||
@RequestBody @Valid CreateCommentCommand request,
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
|
||||
final Long commentId = commentFacade.create(request, authentication.getMemberId());
|
||||
|
||||
return ResponseEntity
|
||||
.created(URI.create("/comments/" + commentId))
|
||||
.build();
|
||||
}
|
||||
|
||||
@PatchMapping("api/comments/{commentId}")
|
||||
public ResponseEntity<Void> updateComment(
|
||||
@PathVariable Long commentId,
|
||||
@RequestBody @Valid UpdateCommentCommand request,
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
|
||||
commentFacade.update(request, commentId, authentication.getMemberId());
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("api/comments/{commentId}")
|
||||
public ResponseEntity<Void> deleteComment(
|
||||
@PathVariable Long commentId,
|
||||
@AuthenticationPrincipal Authentication authentication) {
|
||||
|
||||
commentFacade.delete(commentId, authentication.getMemberId());
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.yam.app.comment.presentation;
|
||||
|
||||
import com.yam.app.comment.application.CommentFacade;
|
||||
import com.yam.app.common.ApiResult;
|
||||
import java.util.List;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class CommentQueryApi {
|
||||
|
||||
private final CommentFacade commentFacade;
|
||||
|
||||
public CommentQueryApi(CommentFacade commentFacade) {
|
||||
this.commentFacade = commentFacade;
|
||||
}
|
||||
|
||||
@GetMapping("/api/comments/{articleId}")
|
||||
public ResponseEntity<ApiResult<List<CommentResponse>>> getComments(
|
||||
@PathVariable Long articleId) {
|
||||
return ResponseEntity.ok(
|
||||
ApiResult.success(commentFacade.findByArticleId(articleId)));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.yam.app.comment.presentation;
|
||||
|
||||
import com.yam.app.member.presentation.MemberResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class CommentResponse {
|
||||
|
||||
private final Long id;
|
||||
private final String content;
|
||||
private final LocalDateTime createAt;
|
||||
private final LocalDateTime modifiedAt;
|
||||
private final Long articleId;
|
||||
private final MemberResponse author;
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.comment.presentation;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public final class CreateCommentCommand {
|
||||
|
||||
@NotNull
|
||||
private Long articleId;
|
||||
|
||||
@Length(max = 120)
|
||||
@NotBlank(message = "댓글 내용은 비어 있을 수 없습니다.")
|
||||
private String content;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.comment.presentation;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public final class UpdateCommentCommand {
|
||||
|
||||
@NotNull
|
||||
private Long commentId;
|
||||
|
||||
@Length(max = 120)
|
||||
@NotBlank(message = "댓글 내용은 비어 있을 수 없습니다.")
|
||||
private String content;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class ApiResult<T> {
|
||||
|
||||
private final boolean success;
|
||||
private final T data;
|
||||
private final String message;
|
||||
|
||||
private ApiResult(boolean success, T data, String message) {
|
||||
this.success = success;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<T> success(T data) {
|
||||
return new ApiResult<>(true, data, null);
|
||||
}
|
||||
|
||||
public static ApiResult<?> error(String message) {
|
||||
return new ApiResult<>(false, null, message);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface Authentication extends Serializable {
|
||||
|
||||
String getCredentials();
|
||||
|
||||
String getRole();
|
||||
|
||||
Long getMemberId();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuthenticationPrincipal {
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public final class DuplicateValueException extends SystemException {
|
||||
|
||||
public DuplicateValueException(String value) {
|
||||
super("These are duplicated value, (value : %s)", value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatus getStatus() {
|
||||
return HttpStatus.CONFLICT;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class EntityNotFoundException extends SystemException {
|
||||
|
||||
public EntityNotFoundException(String format, Object... args) {
|
||||
super(format, args);
|
||||
}
|
||||
|
||||
public HttpStatus getStatus() {
|
||||
return HttpStatus.NOT_FOUND;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum EntityStatus {
|
||||
ALIVE, DELETED;
|
||||
|
||||
public static EntityStatus findStatus(String status) {
|
||||
return Arrays.stream(EntityStatus.values())
|
||||
.filter(s -> s.name().equals(status))
|
||||
.findFirst()
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public final class GlobalApiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleMethod(MethodArgumentNotValidException e) {
|
||||
log.error("MethodArgumentNotValidException", e);
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(ApiResult.error("Invalid argument"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleBind(BindException e) {
|
||||
log.error("BindException", e);
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(ApiResult.error("Invalid argument"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleNotSupported(HttpMediaTypeNotSupportedException e) {
|
||||
log.error("HttpMediaTypeNotSupportedException", e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
.body(ApiResult.error("Http media type not supported"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleNotSupported(
|
||||
HttpRequestMethodNotSupportedException e) {
|
||||
log.error("HttpRequestMethodNotSupportedException", e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(ApiResult.error("Http request method not supported"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
private ResponseEntity<ApiResult<?>> handleException(Exception e) {
|
||||
log.error("Exception", e);
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.body(ApiResult.error("Internal server error"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(EntityNotFoundException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleNotFound(EntityNotFoundException e) {
|
||||
log.error("EntityNotFoundException", e);
|
||||
return ResponseEntity
|
||||
.status(e.getStatus())
|
||||
.body(ApiResult.error("Not found resources"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(DuplicateValueException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleDuplicate(DuplicateValueException e) {
|
||||
log.error("DuplicateValueException", e);
|
||||
return ResponseEntity
|
||||
.status(e.getStatus())
|
||||
.body(ApiResult.error("Duplicated value"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleIllegalState(IllegalStateException e) {
|
||||
log.error("IllegalStateException", e);
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(ApiResult.error(e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(UnauthorizedRequestException.class)
|
||||
private ResponseEntity<ApiResult<?>> handleUnauthorized(UnauthorizedRequestException e) {
|
||||
log.error("UnauthorizedRequestException", e);
|
||||
return ResponseEntity
|
||||
.status(e.getStatus())
|
||||
.body(ApiResult.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public abstract class SystemException extends RuntimeException {
|
||||
|
||||
public SystemException(String format, Object... args) {
|
||||
super(String.format(format, args));
|
||||
}
|
||||
|
||||
public abstract HttpStatus getStatus();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.yam.app.common;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public final class UnauthorizedRequestException extends SystemException {
|
||||
|
||||
public UnauthorizedRequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatus getStatus() {
|
||||
return HttpStatus.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.yam.app.common.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
@Slf4j
|
||||
@Profile({"test", "local"})
|
||||
@Configuration
|
||||
public class EmbeddedRedisConfiguration {
|
||||
|
||||
@Value("${spring.redis.port}")
|
||||
private int redisPort;
|
||||
|
||||
private RedisServer redisServer;
|
||||
|
||||
@PostConstruct
|
||||
public void redisServer() throws IOException {
|
||||
redisServer = RedisServer.builder()
|
||||
.port(redisPort)
|
||||
.build();
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() {
|
||||
if (redisServer != null) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.yam.app.common.configuration;
|
||||
|
||||
import com.yam.app.common.EntityStatus;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
|
||||
@MappedTypes(EntityStatusTypeHandler.class)
|
||||
public final class EntityStatusTypeHandler implements TypeHandler<EntityStatus> {
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i,
|
||||
EntityStatus parameter, JdbcType jdbcType) throws SQLException {
|
||||
ps.setString(i, parameter.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityStatus getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return EntityStatus.findStatus(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityStatus getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return EntityStatus.findStatus(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityStatus getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return EntityStatus.findStatus(cs.getString(columnIndex));
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.yam.app.common.configuration;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.session.data.redis.config.ConfigureRedisAction;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession
|
||||
public class RedisConfiguration {
|
||||
|
||||
@Value("${spring.redis.host}")
|
||||
private String redisHost;
|
||||
|
||||
@Value("${spring.redis.port}")
|
||||
private int redisPort;
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
return new LettuceConnectionFactory(redisHost, redisPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<byte[], byte[]> redisTemplate() {
|
||||
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory());
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConfigureRedisAction configureRedisAction() {
|
||||
return ConfigureRedisAction.NO_OP;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.yam.app.common.configuration;
|
||||
package com.yam.app.configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
@@ -6,10 +6,20 @@ import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
|
||||
@Configuration
|
||||
public class DatabaseConfiguration {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setType(EmbeddedDatabaseType.H2)
|
||||
.addScript("classpath:sql/ddl.sql")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
||||
var factoryBean = new SqlSessionFactoryBean();
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class GenerateMemberEvent {
|
||||
|
||||
private final Long memberId;
|
||||
private final String email;
|
||||
|
||||
public GenerateMemberEvent(Long memberId, String email) {
|
||||
this.memberId = memberId;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
import com.yam.app.common.EntityStatus;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class Member {
|
||||
|
||||
private Long id;
|
||||
private String nickname;
|
||||
private String image;
|
||||
private EntityStatus status = EntityStatus.ALIVE;
|
||||
|
||||
public Member(String nickname, String image) {
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public void changeProfile(String nickname, String image) {
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MemberReader {
|
||||
|
||||
boolean existsByNickname(String nickname);
|
||||
|
||||
Optional<Member> findById(Long memberId);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
public interface MemberRepository {
|
||||
|
||||
Long save(Member entity);
|
||||
|
||||
void update(Member entity);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class RegisterAccountConfirmEvent {
|
||||
|
||||
private final String email;
|
||||
|
||||
public RegisterAccountConfirmEvent(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yam.app.member.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class UpdateAccountEvent {
|
||||
|
||||
private final Long memberId;
|
||||
private final String nickname;
|
||||
private final String image;
|
||||
|
||||
public UpdateAccountEvent(Long memberId, String nickname, String image) {
|
||||
this.memberId = memberId;
|
||||
this.nickname = nickname;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.yam.app.member.infrastructure;
|
||||
|
||||
import com.yam.app.member.domain.GenerateMemberEvent;
|
||||
import com.yam.app.member.domain.Member;
|
||||
import com.yam.app.member.domain.MemberReader;
|
||||
import com.yam.app.member.domain.MemberRepository;
|
||||
import com.yam.app.member.domain.RegisterAccountConfirmEvent;
|
||||
import com.yam.app.member.domain.UpdateAccountEvent;
|
||||
import java.util.UUID;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MemberEventListener {
|
||||
|
||||
private final MemberReader memberReader;
|
||||
private final MemberRepository memberRepository;
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
public MemberEventListener(MemberReader memberReader,
|
||||
MemberRepository memberRepository,
|
||||
ApplicationEventPublisher publisher) {
|
||||
this.memberReader = memberReader;
|
||||
this.memberRepository = memberRepository;
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handle(RegisterAccountConfirmEvent event) {
|
||||
var nickname = event.getEmail().split("@")[0];
|
||||
Long savedEntityId;
|
||||
if (memberReader.existsByNickname(nickname)) {
|
||||
savedEntityId = memberRepository.save(
|
||||
new Member(UUID.randomUUID().toString(), "temp.png"));
|
||||
} else {
|
||||
savedEntityId = memberRepository.save(new Member(nickname, "temp.png"));
|
||||
}
|
||||
publisher.publishEvent(new GenerateMemberEvent(savedEntityId, event.getEmail()));
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handle(UpdateAccountEvent event) {
|
||||
var member = memberReader.findById(event.getMemberId())
|
||||
.orElseThrow(IllegalArgumentException::new);
|
||||
member.changeProfile(event.getNickname(), event.getImage());
|
||||
memberRepository.update(member);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user