getAuthorities() {
+ return List.of(new SimpleGrantedAuthority("ROLE_" + this.name));
+ }
+ }
diff --git a/src/main/java/com/example/trello/util/AuthenticationScheme.java b/src/main/java/com/example/trello/util/AuthenticationScheme.java
new file mode 100644
index 0000000..844dd91
--- /dev/null
+++ b/src/main/java/com/example/trello/util/AuthenticationScheme.java
@@ -0,0 +1,22 @@
+package com.example.trello.util;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum AuthenticationScheme {
+ BEARER("Bearer");
+
+ private final String name;
+
+ /**
+ * Authorization ν€λμ κ°μΌλ‘ μ¬μ©λ prefixλ₯Ό μμ±.
+ *
+ * @param authenticationScheme {@link AuthenticationScheme}
+ * @return μμ±λ prefix
+ */
+ public static String generateType(AuthenticationScheme authenticationScheme) {
+ return authenticationScheme.getName() + " ";
+ }
+}
diff --git a/src/main/java/com/example/trello/util/FileUploadUtil.java b/src/main/java/com/example/trello/util/FileUploadUtil.java
new file mode 100644
index 0000000..2493c98
--- /dev/null
+++ b/src/main/java/com/example/trello/util/FileUploadUtil.java
@@ -0,0 +1,28 @@
+package com.example.trello.util;
+
+import org.apache.commons.io.FilenameUtils;
+
+public class FileUploadUtil {
+ private static final String[] ALLOWED_BOARD_IMAGE = {"jpg", "png", "gif", "jpeg"};
+ private static final String[] ALLOWED_ATTACHMENT_EXTENSIONS = {"jpg", "png", "jpeg", "gif", "pdf", "csv"};
+
+ public static boolean isAllowedExtension(String fileName) {
+ String ext = FilenameUtils.getExtension(fileName).toLowerCase();
+ for (String allowedExt : ALLOWED_BOARD_IMAGE) {
+ if (allowedExt.equals(ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isAllowedAttachmentExtension(String fileName) {
+ String ext = FilenameUtils.getExtension(fileName).toLowerCase();
+ for (String allowedExt : ALLOWED_ATTACHMENT_EXTENSIONS) {
+ if (allowedExt.equals(ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/example/trello/util/JwtProvider.java b/src/main/java/com/example/trello/util/JwtProvider.java
new file mode 100644
index 0000000..d7ddac5
--- /dev/null
+++ b/src/main/java/com/example/trello/util/JwtProvider.java
@@ -0,0 +1,169 @@
+package com.example.trello.util;
+
+import com.example.trello.user.User;
+import com.example.trello.user.UserRepository;
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import jakarta.persistence.EntityNotFoundException;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.function.Function;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class JwtProvider {
+
+ /**
+ * JWT μν¬λ¦Ώ ν€.
+ */
+ @Value("${jwt.secret}")
+ private String secret;
+
+ /**
+ * ν ν° λ§λ£μκ°(λ°λ¦¬μ΄).
+ */
+ @Getter
+ @Value("${jwt.expiry-millis}")
+ private Long expiryMillis;
+
+ /**
+ * Member repository.
+ */
+ private final UserRepository userRepository;
+
+ /**
+ * ν ν° μμ± ν 리ν΄.
+ * μ
λ ₯λ°μ {@link Authentication}μμ μΆμΆν {@code username}μΌλ‘ {@link #generateTokenBy(String)} μ΄μ©νλ€.
+ *
+ * @param authentication μΈμ¦ μλ£λ ν μΈλΆ μ 보
+ * @return μμ±λ ν ν°
+ * @throws EntityNotFoundException μ
λ ₯λ°μ μ΄λ©μΌμ ν΄λΉνλ μ¬μ©μλ₯Ό μ°Ύμ§ λͺ»νμ κ²½μ°
+ */
+ public String generateToken(Authentication authentication) throws EntityNotFoundException {
+ String username = authentication.getName();
+ return this.generateTokenBy(username);
+ }
+
+ /**
+ * μ
λ ₯λ°μ ν ν°μμ {@link Authentication}μ {@code username}μ 리ν΄.
+ *
+ * @param token ν ν°
+ * @return username
+ */
+ public String getUsername(String token) {
+ Claims claims = this.getClaims(token);
+ return claims.getSubject();
+ }
+
+ /**
+ * ν ν°μ΄ μ ν¨νμ§ νμΈ.
+ *
+ * @param token ν ν°
+ * @return μ ν¨ μ¬λΆ.
+ *
+ * - {@code true} - μ ν¨ν¨.
+ * - {@code false} - μ ν¨νμ§ μμ.
+ *
+ */
+ public boolean validToken(String token) throws JwtException {
+ try {
+ return !this.tokenExpired(token);
+ } catch (MalformedJwtException e) {
+ log.error("Invalid JWT token: {}", e.getMessage());
+ } catch (ExpiredJwtException e) {
+ log.error("JWT token is expired: {}", e.getMessage());
+ } catch (UnsupportedJwtException e) {
+ log.error("JWT token is unsupported: {}", e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * μ΄λ©μΌ μ£Όμλ₯Ό μ΄μ©ν΄ ν ν°μ μμ±ν ν 리ν΄.
+ * ν ν° μμ±μλ HS256 μκ³ λ¦¬μ¦μ μ΄μ©.
+ *
+ * @param email μ΄λ©μΌ
+ * @return μμ±λ ν ν°
+ * @throws EntityNotFoundException μ
λ ₯λ°μ μ΄λ©μΌμ ν΄λΉνλ μ¬μ©μλ₯Ό μ°Ύμ§ λͺ»νμ κ²½μ°
+ */
+ private String generateTokenBy(String email) throws EntityNotFoundException {
+ User user = this.userRepository.findByEmailOrElseThrow(email);
+ Date currentDate = new Date();
+ Date expireDate = new Date(currentDate.getTime() + this.expiryMillis);
+
+ return Jwts.builder()
+ .subject(email)
+ .issuedAt(currentDate)
+ .expiration(expireDate)
+ .claim("role", user.getRole())
+ .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), Jwts.SIG.HS256)
+ .compact();
+ }
+
+ /**
+ * JWTμ claim λΆλΆμ μΆμΆ.
+ *
+ * @param token ν ν°
+ * @return {@link Claims}
+ * @see JSON μΉ ν ν°
+ */
+ private Claims getClaims(String token) {
+ if (!StringUtils.hasText(token)) {
+ throw new MalformedJwtException("ν ν°μ΄ λΉμ΄ μμ΅λλ€.");
+ }
+
+ return Jwts.parser()
+ .verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+
+ /**
+ * μ
λ ₯λ°μ ν ν°μ λ§λ£ μ¬λΆ.
+ *
+ * @param token ν ν°
+ * @return λ§λ£ μ¬λΆ
+ *
+ * - {@code true} - λ§λ£λ¨.
+ * - {@code false} - λ§λ£λμ§ μμ.
+ *
+ */
+ private boolean tokenExpired(String token) {
+ final Date expiration = this.getExpirationDateFromToken(token);
+ return expiration.before(new Date());
+ }
+
+ /**
+ * μ
λ ₯ λ°μ ν ν°μ λ§λ£μΌμ 리ν΄.
+ *
+ * @param token ν ν°
+ * @return λ§λ£μΌ
+ */
+ private Date getExpirationDateFromToken(String token) {
+ return this.resolveClaims(token, Claims::getExpiration);
+ }
+
+ /**
+ * ν ν°μ μ
λ ₯ λ°μ λ‘μ§μ μ μ©νκ³ κ·Έ κ²°κ³Όλ₯Ό 리ν΄.
+ *
+ * @param token ν ν°
+ * @param claimsResolver ν ν°μ μ μ©ν λ‘μ§.
+ * @param {@code claimsResolver}μ λ¦¬ν΄ νμ
.
+ * @return {@code T}
+ */
+ private T resolveClaims(String token, Function claimsResolver) {
+ final Claims claims = this.getClaims(token);
+ return claimsResolver.apply(claims);
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace/WorkSpaceRepository.java b/src/main/java/com/example/trello/workspace/WorkSpaceRepository.java
index f8382b1..4741a60 100644
--- a/src/main/java/com/example/trello/workspace/WorkSpaceRepository.java
+++ b/src/main/java/com/example/trello/workspace/WorkSpaceRepository.java
@@ -1,5 +1,9 @@
package com.example.trello.workspace;
+import com.example.trello.common.exception.WorkspaceErrorCode;
+import com.example.trello.common.exception.WorkspaceException;
+import com.example.trello.common.exception.WorkspaceMemberErrorCode;
+import com.example.trello.common.exception.WorkspaceMemberException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -7,7 +11,7 @@
public interface WorkSpaceRepository extends JpaRepository {
default Workspace findByIdOrElseThrow(Long id){
- return findById(id).orElseThrow(()->new RuntimeException());
+ return findById(id).orElseThrow(() -> new WorkspaceException(WorkspaceErrorCode.CAN_NOT_FIND_WORKSPACE_WITH_WORKSPACE_ID));
}
}
diff --git a/src/main/java/com/example/trello/workspace/Workspace.java b/src/main/java/com/example/trello/workspace/Workspace.java
index cf983e9..257d8e7 100644
--- a/src/main/java/com/example/trello/workspace/Workspace.java
+++ b/src/main/java/com/example/trello/workspace/Workspace.java
@@ -2,8 +2,11 @@
import com.example.trello.board.Board;
import com.example.trello.user.User;
+import com.example.trello.workspace_member.WorkspaceMember;
import jakarta.persistence.*;
+import lombok.Builder;
import lombok.Getter;
+import org.hibernate.annotations.DynamicUpdate;
import java.util.List;
@@ -14,15 +17,38 @@ public class Workspace {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- @Column(name = "title")
+ @Column(name = "title", nullable = false)
private String title;
- @Column(name = "description")
+ @Column(name = "description", nullable = false)
private String description;
+ @Column(name = "slack_url")
+ private String slackUrl;
+
@OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
private List boards;
+ @OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List workspaceMembers;
+
@ManyToOne(fetch = FetchType.LAZY)
private User user;
+
+ public Workspace() {
+ }
+
+ @Builder
+ public Workspace(String title, String description, String slackUrl, User user) {
+ this.title = title;
+ this.description = description;
+ this.slackUrl = slackUrl;
+ this.user = user;
+ }
+
+ public void updateWorkspace(String title, String description, String slackUrl) {
+ this.title = title;
+ this.description = description;
+ this.slackUrl = slackUrl;
+ }
}
diff --git a/src/main/java/com/example/trello/workspace/WorkspaceController.java b/src/main/java/com/example/trello/workspace/WorkspaceController.java
new file mode 100644
index 0000000..6af578f
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace/WorkspaceController.java
@@ -0,0 +1,52 @@
+package com.example.trello.workspace;
+
+import com.example.trello.config.auth.UserDetailsImpl;
+import com.example.trello.workspace.dto.UpdateWorkspaceRequestDto;
+import com.example.trello.workspace.dto.WorkspaceRequestDto;
+import com.example.trello.workspace.dto.WorkspaceResponseDto;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/workspaces")
+public class WorkspaceController {
+ private final WorkspaceService workspaceService;
+
+ @PostMapping
+ public ResponseEntity createWorkspace(@Valid @RequestBody WorkspaceRequestDto dto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ WorkspaceResponseDto createdWorkspaceResponseDto = workspaceService.createWorkspace(dto, userDetails.getUser().getId());
+
+ return new ResponseEntity<>(createdWorkspaceResponseDto, HttpStatus.CREATED);
+ }
+
+ @GetMapping
+ public ResponseEntity> viewAllWorkspace(@AuthenticationPrincipal UserDetailsImpl userDetails) {
+ List workspaceResponseDtoList = workspaceService.viewAllWorkspace(userDetails.getUser().getId());
+ return new ResponseEntity<>(workspaceResponseDtoList, HttpStatus.OK);
+ }
+
+ @GetMapping("/{workspaceId}")
+ public ResponseEntity viewWorkspace(@PathVariable Long workspaceId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ WorkspaceResponseDto findWorkspaceResponseDto = workspaceService.viewWorkspace(workspaceId, userDetails.getUser().getId());
+ return new ResponseEntity<>(findWorkspaceResponseDto, HttpStatus.OK);
+ }
+
+ @PatchMapping("/{workspaceId}")
+ public ResponseEntity updateWorkspace(@PathVariable Long workspaceId, @Valid @RequestBody UpdateWorkspaceRequestDto dto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ WorkspaceResponseDto updatedWorkspaceResponseDto = workspaceService.updateWorkspace(workspaceId, dto, userDetails.getUser().getId());
+ return new ResponseEntity<>(updatedWorkspaceResponseDto, HttpStatus.OK);
+ }
+
+ @DeleteMapping("/{workspaceId}")
+ public ResponseEntity deleteWorkspace(@PathVariable Long workspaceId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ workspaceService.deleteWorkspace(workspaceId, userDetails.getUser().getId());
+ return new ResponseEntity<>("μν¬μ€νμ΄μ€κ° μ μμ μΌλ‘ μμ λμμ΅λλ€.", HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace/WorkspaceService.java b/src/main/java/com/example/trello/workspace/WorkspaceService.java
new file mode 100644
index 0000000..d4fa716
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace/WorkspaceService.java
@@ -0,0 +1,110 @@
+package com.example.trello.workspace;
+
+import com.example.trello.common.exception.WorkspaceErrorCode;
+import com.example.trello.common.exception.WorkspaceException;
+import com.example.trello.user.User;
+import com.example.trello.user.UserRepository;
+import com.example.trello.workspace.dto.UpdateWorkspaceRequestDto;
+import com.example.trello.workspace.dto.WorkspaceRequestDto;
+import com.example.trello.workspace.dto.WorkspaceResponseDto;
+
+
+import com.example.trello.workspace_member.WorkspaceMember;
+import com.example.trello.workspace_member.WorkspaceMemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.example.trello.user.enums.Role.ADMIN;
+import static com.example.trello.workspace_member.WorkspaceMemberRole.WORKSPACE;
+
+@Service
+@RequiredArgsConstructor
+public class WorkspaceService {
+ private final UserRepository userRepository;
+ private final WorkSpaceRepository workSpaceRepository;
+ private final WorkspaceMemberRepository workspaceMemberRepository;
+
+ @Transactional
+ public WorkspaceResponseDto createWorkspace(WorkspaceRequestDto dto, Long loginUserId) {
+ User loginUser = userRepository.findByIdOrElseThrow(loginUserId);
+
+ if (loginUser.getRole() != ADMIN) {
+ throw new WorkspaceException(WorkspaceErrorCode.ONLY_ADMIN_CAN_CREATE_WORKSPACE);
+ }
+
+ Workspace workspace = Workspace.builder()
+ .title(dto.getTitle())
+ .description(dto.getDescription())
+ .slackUrl(dto.getSlackUrl())
+ .user(loginUser)
+ .build();
+
+ WorkspaceMember workspaceMember = WorkspaceMember.builder()
+ .user(loginUser)
+ .workspace(workspace)
+ .role(WORKSPACE)
+ .build();
+
+ workSpaceRepository.save(workspace);
+ workspaceMemberRepository.save(workspaceMember);
+
+ return WorkspaceResponseDto.toDto(workspace);
+ }
+
+ @Transactional(readOnly = true)
+ public List viewAllWorkspace(Long loginUserId) {
+ List WorkspaceMemberListByUser = workspaceMemberRepository.findByUserId(loginUserId);
+
+ List workspaceList = new ArrayList<>();
+ for (WorkspaceMember workspaceMember : WorkspaceMemberListByUser) {
+ workspaceList.add(workspaceMember.getWorkspace());
+ }
+
+ return workspaceList
+ .stream()
+ .map(WorkspaceResponseDto::toDto)
+ .toList();
+ }
+
+ @Transactional(readOnly = true)
+ public WorkspaceResponseDto viewWorkspace(Long workspaceId, Long loginUserId) {
+ WorkspaceMember findWorkspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(loginUserId, workspaceId);
+
+ Workspace workspace = findWorkspaceMember.getWorkspace();
+
+ return WorkspaceResponseDto.toDto(workspace);
+ }
+
+ @Transactional
+ public WorkspaceResponseDto updateWorkspace(Long workspaceId, UpdateWorkspaceRequestDto dto, Long loginUserId) {
+ WorkspaceMember findWorkspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(loginUserId, workspaceId);
+
+ if (findWorkspaceMember.getRole() != WORKSPACE) {
+ throw new WorkspaceException(WorkspaceErrorCode.ONLY_WORKSPACE_ROLE_CAN_HANDLE_WORKSPACE);
+ }
+
+ Workspace workspace = findWorkspaceMember.getWorkspace();
+
+ workspace.updateWorkspace(dto.getTitle(), dto.getDescription(), dto.getSlackUrl());
+
+ return WorkspaceResponseDto.toDto(workspace);
+ }
+
+ @Transactional
+ public void deleteWorkspace(Long workspaceId, Long loginUserId) {
+ WorkspaceMember findWorkspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(loginUserId, workspaceId);
+
+ if (findWorkspaceMember.getRole() != WORKSPACE) {
+ throw new WorkspaceException(WorkspaceErrorCode.ONLY_WORKSPACE_ROLE_CAN_HANDLE_WORKSPACE);
+ }
+
+ Workspace workspace = findWorkspaceMember.getWorkspace();
+
+ workSpaceRepository.delete(workspace);
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace/dto/UpdateWorkspaceRequestDto.java b/src/main/java/com/example/trello/workspace/dto/UpdateWorkspaceRequestDto.java
new file mode 100644
index 0000000..8352c97
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace/dto/UpdateWorkspaceRequestDto.java
@@ -0,0 +1,24 @@
+package com.example.trello.workspace.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+
+@Getter
+public class UpdateWorkspaceRequestDto {
+ @NotBlank(message = "title μ Null μΌ μ μμ΅λλ€.")
+ @Size(min = 1, max = 50, message = "title ν¬κΈ°λ 1μμ 50μ¬μ΄μ¬μΌν©λλ€.")
+ private String title;
+
+ @NotBlank(message = "description μ Null μΌ μ μμ΅λλ€.")
+ @Size(min = 1, max = 255, message = "title ν¬κΈ°λ 1μμ 255μ¬μ΄μ¬μΌν©λλ€.")
+ private String description;
+
+ private String slackUrl;
+
+ public UpdateWorkspaceRequestDto(String title, String description, String slackUrl) {
+ this.title = title;
+ this.description = description;
+ this.slackUrl = slackUrl;
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace/dto/WorkspaceRequestDto.java b/src/main/java/com/example/trello/workspace/dto/WorkspaceRequestDto.java
new file mode 100644
index 0000000..d6f89c8
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace/dto/WorkspaceRequestDto.java
@@ -0,0 +1,24 @@
+package com.example.trello.workspace.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+
+@Getter
+public class WorkspaceRequestDto {
+ @NotBlank(message = "title μ Null μΌ μ μμ΅λλ€.")
+ @Size(min = 1, max = 50, message = "title ν¬κΈ°λ 1μμ 50μ¬μ΄μ¬μΌν©λλ€.")
+ private String title;
+
+ @NotBlank(message = "description μ Null μΌ μ μμ΅λλ€.")
+ @Size(min = 1, max = 255, message = "title ν¬κΈ°λ 1μμ 255μ¬μ΄μ¬μΌν©λλ€.")
+ private String description;
+
+ private String slackUrl;
+
+ public WorkspaceRequestDto(String title, String description, String slackUrl) {
+ this.title = title;
+ this.description = description;
+ this.slackUrl = slackUrl;
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace/dto/WorkspaceResponseDto.java b/src/main/java/com/example/trello/workspace/dto/WorkspaceResponseDto.java
new file mode 100644
index 0000000..dbeef98
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace/dto/WorkspaceResponseDto.java
@@ -0,0 +1,23 @@
+package com.example.trello.workspace.dto;
+
+import com.example.trello.workspace.Workspace;
+import lombok.Getter;
+
+@Getter
+public class WorkspaceResponseDto {
+ private Long workspaceId;
+ private String title;
+ private String description;
+ private String slackUrl;
+
+ public WorkspaceResponseDto(Long workspaceId, String title, String description, String slackUrl) {
+ this.workspaceId = workspaceId;
+ this.title = title;
+ this.description = description;
+ this.slackUrl = slackUrl;
+ }
+
+ public static WorkspaceResponseDto toDto(Workspace workspace) {
+ return new WorkspaceResponseDto(workspace.getId(), workspace.getTitle(), workspace.getDescription(), workspace.getSlackUrl());
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/WorkspaceMember.java b/src/main/java/com/example/trello/workspace_member/WorkspaceMember.java
new file mode 100644
index 0000000..a72d016
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/WorkspaceMember.java
@@ -0,0 +1,40 @@
+package com.example.trello.workspace_member;
+
+import com.example.trello.user.User;
+import com.example.trello.workspace.Workspace;
+import jakarta.persistence.*;
+import lombok.Builder;
+import lombok.Getter;
+
+@Entity
+@Getter
+public class WorkspaceMember {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "role", nullable = false)
+ @Enumerated(EnumType.STRING)
+ private WorkspaceMemberRole role;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private User user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Workspace workspace;
+
+ public WorkspaceMember() {
+ }
+
+ @Builder
+ public WorkspaceMember(WorkspaceMemberRole role, User user, Workspace workspace) {
+ this.role = role;
+ this.user = user;
+ this.workspace = workspace;
+ }
+
+ public void updateRole(WorkspaceMemberRole role) {
+ this.role = role;
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/WorkspaceMemberController.java b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberController.java
new file mode 100644
index 0000000..a8583e6
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberController.java
@@ -0,0 +1,32 @@
+package com.example.trello.workspace_member;
+
+import com.example.trello.config.auth.UserDetailsImpl;
+import com.example.trello.workspace_member.dto.UpdateWorkspaceMemberRoleDto;
+import com.example.trello.workspace_member.dto.WorkspaceMemberRequestDto;
+import com.example.trello.workspace_member.dto.WorkspaceMemberResponseDto;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/workspaces/{workspaceId}")
+public class WorkspaceMemberController {
+
+ private final WorkspaceMemberService workspaceMemberService;
+
+ @PostMapping("/member-invite")
+ public ResponseEntity inviteWorkspaceUser(@PathVariable Long workspaceId, @Valid @RequestBody WorkspaceMemberRequestDto dto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ WorkspaceMemberResponseDto workspaceMemberResponseDto = workspaceMemberService.inviteWorkspaceMember(workspaceId, dto, userDetails.getUser().getId());
+ return new ResponseEntity<>(workspaceMemberResponseDto, HttpStatus.CREATED);
+ }
+
+ @PatchMapping("/member-role")
+ public ResponseEntity updateWorkspaceUserRole(@PathVariable Long workspaceId, @Valid @RequestBody UpdateWorkspaceMemberRoleDto dto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ workspaceMemberService.updateWorkspaceMemberRole(workspaceId, dto, userDetails.getUser().getId());
+ return new ResponseEntity<>("κΆνμ΄ " + dto.getRole() + "λ‘ λ³κ²½λμμ΅λλ€.", HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRepository.java b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRepository.java
new file mode 100644
index 0000000..85229a1
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRepository.java
@@ -0,0 +1,32 @@
+package com.example.trello.workspace_member;
+
+import com.example.trello.common.exception.WorkspaceMemberErrorCode;
+import com.example.trello.common.exception.WorkspaceMemberException;
+import com.example.trello.user.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+
+@Repository
+public interface WorkspaceMemberRepository extends JpaRepository {
+ List findByUserId(Long userId);
+ Optional findByUserIdAndWorkspaceId(Long userId, Long workspaceId);
+ Optional findByIdAndWorkspaceId(Long id, Long workspaceId);
+ Boolean existsByUserIdAndWorkspaceId(Long userId, Long workspaceId);
+
+ default WorkspaceMember findByIdOrElseThrow(Long id){
+ return findById(id).orElseThrow(() -> new WorkspaceMemberException(WorkspaceMemberErrorCode.CAN_NOT_FIND_WORKSPACEMEMBER_WITH_WORKSPACEMEMBER_ID));
+ }
+
+ default WorkspaceMember findByIdAndWorkspaceIdOrElseThrow(Long id, Long workspaceId){
+ return findByIdAndWorkspaceId(id, workspaceId).orElseThrow(() -> new WorkspaceMemberException(WorkspaceMemberErrorCode.IS_NOT_WORKSPACEMEMBER));
+ }
+
+ default WorkspaceMember findByUserIdAndWorkspaceIdOrElseThrow(Long userId, Long workspaceId) {
+ return findByUserIdAndWorkspaceId(userId, workspaceId).orElseThrow(() -> new WorkspaceMemberException(WorkspaceMemberErrorCode.IS_NOT_WORKSPACEMEMBER));
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRole.java b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRole.java
new file mode 100644
index 0000000..f9722cc
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberRole.java
@@ -0,0 +1,7 @@
+package com.example.trello.workspace_member;
+
+public enum WorkspaceMemberRole {
+ WORKSPACE,
+ BOARD,
+ READ_ONLY
+}
diff --git a/src/main/java/com/example/trello/workspace_member/WorkspaceMemberService.java b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberService.java
new file mode 100644
index 0000000..e664846
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/WorkspaceMemberService.java
@@ -0,0 +1,87 @@
+package com.example.trello.workspace_member;
+
+import com.example.trello.common.exception.WorkspaceErrorCode;
+import com.example.trello.common.exception.WorkspaceException;
+import com.example.trello.common.exception.WorkspaceMemberErrorCode;
+import com.example.trello.common.exception.WorkspaceMemberException;
+import com.example.trello.notification.NotificationService;
+import com.example.trello.user.User;
+import com.example.trello.user.UserRepository;
+import com.example.trello.workspace.WorkSpaceRepository;
+import com.example.trello.workspace.Workspace;
+import com.example.trello.workspace_member.dto.UpdateWorkspaceMemberRoleDto;
+import com.example.trello.workspace_member.dto.WorkspaceMemberRequestDto;
+import com.example.trello.workspace_member.dto.WorkspaceMemberResponseDto;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import static com.example.trello.common.exception.WorkspaceMemberErrorCode.CAN_NOT_READ_ROLE;
+import static com.example.trello.notification.NotificationType.ADD_MEMBER;
+import static com.example.trello.user.enums.Role.ADMIN;
+import static com.example.trello.workspace_member.WorkspaceMemberRole.READ_ONLY;
+import static com.example.trello.workspace_member.WorkspaceMemberRole.WORKSPACE;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WorkspaceMemberService {
+
+ private final WorkspaceMemberRepository workspaceMemberRepository;
+ private final WorkSpaceRepository workSpaceRepository;
+ private final UserRepository userRepository;
+ private final NotificationService notificationService;
+
+ @Transactional
+ public WorkspaceMemberResponseDto inviteWorkspaceMember(Long workspaceId, WorkspaceMemberRequestDto dto, Long loginUserId) {
+ WorkspaceMember findWorkspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(loginUserId, workspaceId);
+
+ if (findWorkspaceMember.getRole() != WORKSPACE) {
+ throw new WorkspaceMemberException(WorkspaceMemberErrorCode.ONLY_WORKSPACE_ROLE_CAN_INVITE);
+ }
+
+ User findUser = userRepository.findByEmailOrElseThrow(dto.getEmail());
+ Workspace workspace = findWorkspaceMember.getWorkspace();
+
+ if (workspaceMemberRepository.existsByUserIdAndWorkspaceId(findUser.getId(), workspace.getId())) {
+ throw new WorkspaceMemberException(WorkspaceMemberErrorCode.ALREADY_MEMBER);
+ }
+
+ WorkspaceMember workspaceMember = WorkspaceMember.builder()
+ .user(findUser)
+ .workspace(workspace)
+ .role(READ_ONLY)
+ .build();
+
+ workspaceMemberRepository.save(workspaceMember);
+ notificationService.sendSlack(ADD_MEMBER, workspace);
+
+ return WorkspaceMemberResponseDto.toDto(workspaceMember);
+ }
+
+ @Transactional
+ public void updateWorkspaceMemberRole(Long workspaceId, UpdateWorkspaceMemberRoleDto dto, Long loginUserId) {
+ WorkspaceMember findWorkspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(loginUserId, workspaceId);
+
+ if (dto.getRole() == WORKSPACE && findWorkspaceMember.getUser().getRole() != ADMIN) {
+ throw new WorkspaceException(WorkspaceErrorCode.ONLY_ADMIN_CAN_UPDATE_MEMBER_ROLE_TO_WORKSPACE);
+ }
+
+ if (findWorkspaceMember.getRole() != WORKSPACE) {
+ throw new WorkspaceMemberException(WorkspaceMemberErrorCode.ONLY_WORKSPACE_ROLE_CAN_UPDATE_MEMBER_ROLE);
+ }
+
+ WorkspaceMember roleUpdatedWorkspaceMember = workspaceMemberRepository.findByIdAndWorkspaceIdOrElseThrow(dto.getWorkspaceMemberId(), workspaceId);
+
+ roleUpdatedWorkspaceMember.updateRole(dto.getRole());
+ log.info("{}μ μν μ΄ {}λ‘ λ³κ²½λμμ΅λλ€.", roleUpdatedWorkspaceMember.getUser().getNickname(), dto.getRole());
+ }
+
+ public void checkReadRole(Long userId, Long workspaceId) {
+ WorkspaceMember workspaceMember = workspaceMemberRepository.findByUserIdAndWorkspaceIdOrElseThrow(userId, workspaceId);
+ if(workspaceMember.getRole()==READ_ONLY) {
+ throw new WorkspaceMemberException(CAN_NOT_READ_ROLE);
+ }
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/dto/UpdateWorkspaceMemberRoleDto.java b/src/main/java/com/example/trello/workspace_member/dto/UpdateWorkspaceMemberRoleDto.java
new file mode 100644
index 0000000..17cba52
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/dto/UpdateWorkspaceMemberRoleDto.java
@@ -0,0 +1,20 @@
+package com.example.trello.workspace_member.dto;
+
+import com.example.trello.workspace_member.WorkspaceMemberRole;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+
+@Getter
+public class UpdateWorkspaceMemberRoleDto {
+ @NotNull
+ private Long workspaceMemberId;
+
+ @NotNull
+ private WorkspaceMemberRole role;
+
+ public UpdateWorkspaceMemberRoleDto(Long workspaceMemberId, WorkspaceMemberRole role) {
+ this.workspaceMemberId = workspaceMemberId;
+ this.role = role;
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberRequestDto.java b/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberRequestDto.java
new file mode 100644
index 0000000..34d3951
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberRequestDto.java
@@ -0,0 +1,16 @@
+package com.example.trello.workspace_member.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Getter;
+
+@Getter
+public class WorkspaceMemberRequestDto {
+ @NotBlank
+ @Pattern(regexp = "^[A-Za-z0-9.!@#$+]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$", message = "μ΄λ©μΌ νμμ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€. λ€μ μ
λ ₯ν΄μ£ΌμΈμ")
+ private String email;
+
+ public WorkspaceMemberRequestDto(String email) {
+ this.email = email;
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberResponseDto.java b/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberResponseDto.java
new file mode 100644
index 0000000..5419efd
--- /dev/null
+++ b/src/main/java/com/example/trello/workspace_member/dto/WorkspaceMemberResponseDto.java
@@ -0,0 +1,22 @@
+package com.example.trello.workspace_member.dto;
+
+import com.example.trello.workspace_member.WorkspaceMember;
+import com.example.trello.workspace_member.WorkspaceMemberRole;
+import lombok.Getter;
+
+@Getter
+public class WorkspaceMemberResponseDto {
+ private Long userId;
+ private Long workspaceId;
+ private WorkspaceMemberRole role;
+
+ public WorkspaceMemberResponseDto(Long userId, Long workspaceId, WorkspaceMemberRole role) {
+ this.userId = userId;
+ this.workspaceId = workspaceId;
+ this.role = role;
+ }
+
+ public static WorkspaceMemberResponseDto toDto(WorkspaceMember workspaceMember) {
+ return new WorkspaceMemberResponseDto(workspaceMember.getUser().getId(), workspaceMember.getWorkspace().getId(), workspaceMember.getRole());
+ }
+}
diff --git a/src/main/java/com/example/trello/workspace_user/WorkspaceUser.java b/src/main/java/com/example/trello/workspace_user/WorkspaceUser.java
deleted file mode 100644
index 4aa39ef..0000000
--- a/src/main/java/com/example/trello/workspace_user/WorkspaceUser.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.example.trello.workspace_user;
-
-import com.example.trello.user.User;
-import jakarta.persistence.*;
-import lombok.Getter;
-
-@Entity
-@Getter
-public class WorkspaceUser {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @ManyToOne(fetch = FetchType.LAZY)
- private User user;
-
- @ManyToOne(fetch = FetchType.LAZY)
- private WorkspaceUser workspaceUser;
-
-
-}
diff --git a/src/main/java/com/example/trello/workspace_user/WorkspaceUserRepository.java b/src/main/java/com/example/trello/workspace_user/WorkspaceUserRepository.java
deleted file mode 100644
index 89adcd2..0000000
--- a/src/main/java/com/example/trello/workspace_user/WorkspaceUserRepository.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.trello.workspace_user;
-
-import com.example.trello.workspace.Workspace;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-@Repository
-public interface WorkspaceUserRepository extends JpaRepository {
-
- default WorkspaceUser findByIdOrElseThrow(Long id){
- return findById(id).orElseThrow(()->new RuntimeException());
- }
-}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index af41afb..697e26a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -4,10 +4,16 @@ spring.datasource.url=jdbc:mysql://localhost:3306/${DB_NAME}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-spring.jpa.hibernate.ddl-auto=none
+spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
-
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
+
+spring.servlet.multipart.enabled=true
+spring.servlet.multipart.max-file-size=5MB
+spring.servlet.multipart.max-request-size=5MB
+
+jwt.secret="036c4fe3ec667532545b9e8fa7e2a98a22f439dff102623c097715060e2da68c"
+jwt.expiry-millis=3600000
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..0e44c6f
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,9 @@
+cloud:
+ aws:
+ s3:
+ bucket: ${BUCKET_NAME}
+ stack.auto: false
+ region.static: ap-northeast-2
+ credentials:
+ accessKey: ${BUCKET_ACCESSKEY}
+ secretKey: ${BUCKET_SECRETKEY}
\ No newline at end of file
diff --git a/src/test/java/com/example/trello/TrelloApplicationTests.java b/src/test/java/com/example/trello/TrelloApplicationTests.java
deleted file mode 100644
index 6281f09..0000000
--- a/src/test/java/com/example/trello/TrelloApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.trello;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class TrelloApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}