Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// AWS
implementation 'software.amazon.awssdk:s3:2.25.4'

// GitHub API
implementation 'org.kohsuke:github-api:1.321'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class DecisionListDTO {
private String name;

@Schema(description = "적용사항 개수", example = "3")
private Long applicationCount;
private Integer applicationCount;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public class Decision extends BaseEntity {
@Column(name = "is_created")
private Boolean isCreated;

// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true)
// private final List<Application> applications = new ArrayList<>();
@OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<Application> applications = new ArrayList<>();
//
// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true)
// private final List<DecisionTimeline> decisionTimelines = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@
import com.whylog.server.domain.team.dto.TeamRequest;
import com.whylog.server.domain.team.dto.TeamResponse;
import com.whylog.server.domain.team.service.TeamCommandService;
import com.whylog.server.domain.team.service.TeamQueryService;
import com.whylog.server.global.apiPayload.ApiResponse;
import com.whylog.server.global.auth.annotation.CurrentMember;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Encoding;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequestMapping("/api/teams")
Expand All @@ -25,33 +34,77 @@
public class TeamController {

private final TeamCommandService teamCommandService;
private final TeamQueryService teamQueryService;

@GetMapping("/{teamId}/decisions")
@Operation(summary = "결정사항 목록 조회 API", description = "특정 팀의 결정사항 목록을 조회하는 API입니다.")
public ApiResponse<DecisionResponse.DecisionListDTO> getDecisions(
public ApiResponse<List<DecisionResponse.DecisionListDTO>> getDecisions(
@PathVariable Long teamId) {
return ApiResponse.onSuccess(null);
List<DecisionResponse.DecisionListDTO> decisions = teamQueryService.decisions(teamId);
return ApiResponse.onSuccess(decisions);
}

@PostMapping("/{teamId}/invitations")
@Operation(summary = "팀 초대 API", description = "특정 팀에 사용자를 초대하는 API입니다.")
@Operation(summary = "팀 초대 API", description = """

특정 사용자를 팀에 초대합니다. 팀 초대를 하면 상대는 즉시 팀원으로 추가됩니다.

| 상황 | HTTP Status | Code | Message |
| --- | --- | --- | --- |
| 성공 | 200 OK | COMMON200 | 성공입니다. |
| 이미 팀에 속한 사용자 | 409 Conflict | TEAM_409 | 이미 팀에 속한 사용자입니다. |
| 팀 없음 | 404 Not Found | TEAM_404 | 존재하지 않는 팀입니다. |
| 사용자 없음 | 404 Not Found | MEMBER_404 | 찾을 수 없는 유저입니다. |

""")
public ApiResponse<TeamResponse.InvitationResponseDTO> sendInvitation(
@PathVariable Long teamId,
@Valid @RequestBody TeamRequest.InvitationDTO request) {
return ApiResponse.onSuccess(null);
TeamResponse.InvitationResponseDTO result = teamCommandService.invite(teamId, request);
return ApiResponse.onSuccess(result);
}

@PostMapping
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "팀 생성 API", description = """
팀을 생성합니다.
팀명은 50글자 미만입니다.
팀 생성과 동시에 팀에 참여합니다. ( 따로 호출 X )
""")

## 설명
팀을 생성합니다. 해당 api를 호출한 사람은 팀에 참여합니다.

## API 호출 방법

`multipart/form-data`로 요청합니다.
`request` 파트는 JSON Blob으로 추가하고, `image` 파트는 선택값입니다.

| Part name | Required | Value |
| --- | --- | --- |
| `request` | O | `application/json` 타입의 JSON Blob |
| `image` | X | 이미지 파일 |

### request JSON

```json
{
"name": "팀명이어떻게다마고치"
}
```

`Content-Type`은 직접 지정하지 않습니다. 브라우저가 boundary를 포함한 `multipart/form-data` 값을 자동으로 생성해야 합니다.

""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
schema = @Schema(implementation = TeamRequest.TeamCreateMultipartDTO.class),
encoding = @Encoding(name = "request", contentType = MediaType.APPLICATION_JSON_VALUE)
)
)
)
public ApiResponse<TeamResponse.TeamCreateResponseDTO> createTeam(
@Parameter(hidden = true) @CurrentMember Long memberId,
@Valid @RequestBody TeamRequest.TeamCreateDTO request
@Parameter(hidden = true) @Valid @RequestPart("request") TeamRequest.TeamCreateDTO request,
@Parameter(hidden = true) @RequestPart(value = "image", required = false) MultipartFile image
) {
return ApiResponse.onSuccess(teamCommandService.createTeam(memberId, request));
return ApiResponse.onSuccess(teamCommandService.createTeam(memberId, request, image));
}

}
16 changes: 15 additions & 1 deletion src/main/java/com/whylog/server/domain/team/dto/TeamRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class TeamRequest {
public static class InvitationDTO {

@Schema(description = "초대받을 사용자 이메일", example = "member@example.com")
@NotBlank @Email
@Email @NotBlank
private String memberEmail;
}

Expand All @@ -33,4 +33,18 @@ public static class TeamCreateDTO {

}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Schema(description = "팀 생성 multipart 요청")
public static class TeamCreateMultipartDTO {

@Schema(description = "팀 생성 요청 JSON")
private TeamCreateDTO request;

@Schema(description = "팀 이미지 파일", type = "string", format = "binary")
private String image;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public static class TeamCreateResponseDTO {
@Schema(description = "팀명", example = "팀명이 어떻게 다마고치")
private String name;

@Schema(description = "팀 이미지 URL", example = "https://cdn.whylog.com/teams/team-image.png")
private String imageUrl;

}

}
9 changes: 7 additions & 2 deletions src/main/java/com/whylog/server/domain/team/entity/Team.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ public class Team extends BaseEntity {
@Column(length = 50, nullable = false, unique = true)
private String name;

@Column(name = "image")
private String image; // s3 key

@Builder
private Team(String name) {
private Team(String name, String image) {
this.name = name;
this.image = image;
}

public static Team create(TeamRequest.TeamCreateDTO dto) {
public static Team create(TeamRequest.TeamCreateDTO dto, String image) {
return Team.builder()
.name(dto.getName())
.image(image)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public enum TeamErrorCode implements BaseErrorCode {
// 400 Bad Request
TEAM_NAME_LENGTH(HttpStatus.BAD_REQUEST, "TEAM_400", "팀명 길이는 50글자 미만이어야 합니다."),

// 409 Conflict
TEAM_MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "TEAM_409", "이미 팀에 속한 사용자입니다."),

// 422 Unprocessable Entity
TEAM_NAME_ALREADY_EXISTS(HttpStatus.UNPROCESSABLE_ENTITY, "TEAM_420", "이미 존재하는 팀명입니다.")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
import org.springframework.data.jpa.repository.JpaRepository;

public interface TeamMemberRepository extends JpaRepository<TeamMember, TeamMemberId> {
boolean existsByMemberIdAndTeamIdAndActiveTrue(Long memberId, Long teamId);

boolean existsByTeamIdAndMemberIdAndActiveTrue(Long teamId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package com.whylog.server.domain.team.repository;

import com.whylog.server.domain.decision.entity.Decision;
import com.whylog.server.domain.team.entity.Team;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface TeamRepository extends JpaRepository<Team, Long> {

Boolean existsByName(String teamName);

@Query("""
SELECT DISTINCT d
FROM Decision d
JOIN FETCH d.meeting m
LEFT JOIN FETCH d.applications
WHERE m.team.id = :teamId
""")
List<Decision> findDecisions(@Param("teamId") Long teamId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
import com.whylog.server.domain.user.entity.Member;
import com.whylog.server.domain.user.service.MemberUseCase;
import com.whylog.server.global.apiPayload.exception.handler.ErrorHandler;
import com.whylog.server.global.external.s3.ImageType;
import com.whylog.server.global.external.s3.S3Client;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
Expand All @@ -22,9 +25,11 @@ public class TeamCommandService {
private final TeamRepository teamRepository;
private final TeamMemberRepository teamMemberRepository;
private final MemberUseCase memberUseCase;
private final TeamUseCase teamUseCase;
private final S3Client s3Client;

@Transactional
public TeamResponse.TeamCreateResponseDTO createTeam(Long memberId, TeamRequest.TeamCreateDTO request){
public TeamResponse.TeamCreateResponseDTO createTeam(Long memberId, TeamRequest.TeamCreateDTO request, MultipartFile image){

// 팀명 이미 존재하면 예외 발생
if(teamRepository.existsByName(request.getName())){
Expand All @@ -36,8 +41,13 @@ public TeamResponse.TeamCreateResponseDTO createTeam(Long memberId, TeamRequest.
throw new ErrorHandler(TeamErrorCode.TEAM_NAME_LENGTH);
}

String imageKey = null;
if( image != null && !image.isEmpty()){
imageKey = s3Client.uploadFile(image, ImageType.TEAM_IMAGE);
}

// 팀 생성 및 저장
Team team = Team.create(request);
Team team = Team.create(request, imageKey);
teamRepository.save(team);

// 팀원으로 등록
Expand All @@ -47,12 +57,36 @@ public TeamResponse.TeamCreateResponseDTO createTeam(Long memberId, TeamRequest.
return TeamResponse.TeamCreateResponseDTO.builder()
.teamId(team.getId())
.name(team.getName())
.imageUrl(s3Client.getFileUrl(team.getImage()))
.build();
}

private void addMember(Team team, Member member, TeamRole role){
@Transactional
public TeamResponse.InvitationResponseDTO invite(Long teamId, TeamRequest.InvitationDTO request){

// 데이터 조회
Member member = memberUseCase.findMemberByEmail(request.getMemberEmail());
Team team = teamUseCase.findTeamById(teamId);

// 이미 초대된 경우는 예외처리
if (teamMemberRepository.existsByTeamIdAndMemberIdAndActiveTrue(team.getId(), member.getId())) {
throw new ErrorHandler(TeamErrorCode.TEAM_MEMBER_ALREADY_EXISTS);
}

// 팀원 추가
TeamMember teamMember = addMember(team, member, TeamRole.MEMBER);

return TeamResponse.InvitationResponseDTO.builder()
.teamId(teamMember.getTeam().getId())
.memberEmail(teamMember.getMember().getEmail())
.build();
}

private TeamMember addMember(Team team, Member member, TeamRole role){
TeamMember teamMember = TeamMember.create(team, member, role);
teamMemberRepository.save(teamMember);
return teamMemberRepository.save(teamMember);
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.whylog.server.domain.team.service;

import com.whylog.server.domain.decision.dto.DecisionResponse;
import com.whylog.server.domain.decision.entity.Decision;
import com.whylog.server.domain.team.repository.TeamRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class TeamQueryService {

private final TeamRepository teamRepository;

@Transactional(readOnly = true)
public List<DecisionResponse.DecisionListDTO> decisions( Long teamId ){

List<Decision> decisions = teamRepository.findDecisions(teamId);

return decisions.stream()
.map(d -> DecisionResponse.DecisionListDTO.builder()
.decisionId(d.getId())
.name(d.getMeeting().getName())
.applicationCount( d.getApplications().size() )
.build()
).toList();
}

}
Loading
Loading