diff --git a/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java b/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java index 07cedf273..009c08df1 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java +++ b/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java @@ -108,6 +108,6 @@ public Optional findExistCommentById(Long commentId) { .userId(rs.getLong("user_id")) .issueId(rs.getLong("issue_id")) .content(rs.getString("content")) - .createdAt(rs.getTimestamp("created_At").toLocalDateTime()) + .createdAt(rs.getTimestamp("created_at").toLocalDateTime()) .build()); } diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java index ad90994f4..0ee0d3549 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java @@ -53,8 +53,11 @@ public enum ErrorCode implements StatusCode { // -- [Issue] -- // DUPLICATE_OBJECT_FOUND(HttpStatus.BAD_REQUEST, "중복된 항목 선택입니다."), + NOT_EXIST_ISSUE(HttpStatus.BAD_REQUEST, "존재하지 않는 이슈입니다."), + ALREADY_DELETED_ISSUE(HttpStatus.BAD_REQUEST, "이미 삭제된 이슈입니다."), NOT_FOUND_ISSUES(HttpStatus.BAD_REQUEST, "이슈를 찾을 수 없습니다."); + private HttpStatus status; private String message; diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java b/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java index 8d5868201..35e17c872 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java @@ -2,9 +2,16 @@ import static codesquad.issueTracker.global.exception.SuccessCode.*; +import codesquad.issueTracker.issue.dto.IssueLabelResponseDto; +import codesquad.issueTracker.issue.dto.IssueMilestoneResponseDto; +import codesquad.issueTracker.issue.dto.IssueOptionResponseDto; +import codesquad.issueTracker.issue.dto.IssueResponseDto; +import codesquad.issueTracker.issue.dto.IssueUserResponseDto; +import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -35,12 +42,42 @@ public class IssueController { @PostMapping("/issues") public ApiResponse postIssues(@Valid @RequestBody IssueWriteRequestDto request, - HttpServletRequest httpServletRequest) { + HttpServletRequest httpServletRequest) { Long id = Long.parseLong(String.valueOf(httpServletRequest.getAttribute("userId"))); issueService.save(request, id); return ApiResponse.success(SUCCESS.getStatus(), SUCCESS.getMessage()); } + + @GetMapping("/issues/labels") + public ApiResponse> getIssueLabels() { + List labels = issueService.getIssueLabels(); + return ApiResponse.success(SUCCESS.getStatus(), labels); + } + + @GetMapping("/issues/milestones") + public ApiResponse> getIssueMilestones() { + List milestones = issueService.getIssueMilestones(); + return ApiResponse.success(SUCCESS.getStatus(), milestones); + } + + @GetMapping("/issues/participants") + public ApiResponse> getIssueUsers() { + List participants = issueService.getIssueUsers(); + return ApiResponse.success(SUCCESS.getStatus(), participants); + } + + @GetMapping("/issues/{issueId}") + public ApiResponse getIssue(@PathVariable Long issueId) { + IssueResponseDto issueResponseDto = issueService.getIssueById(issueId); + return ApiResponse.success(SUCCESS.getStatus(), issueResponseDto); + } + + @GetMapping("/issues/{issueId}/options") + public ApiResponse getIssueOptions(@PathVariable Long issueId) { + IssueOptionResponseDto issueOptionResponseDto = issueService.getIssueOptions(issueId); + return ApiResponse.success(SUCCESS.getStatus(), issueOptionResponseDto); + @PatchMapping("/issues/status") public ApiResponse patchStatus(@RequestBody ModifyIssueStatusRequestDto request) { issueService.modifyIssueStatus(request); diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueLabelResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueLabelResponseDto.java new file mode 100644 index 000000000..dd7a0a145 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueLabelResponseDto.java @@ -0,0 +1,30 @@ +package codesquad.issueTracker.issue.dto; + +import codesquad.issueTracker.label.vo.LabelVo; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueLabelResponseDto { + private Long id; + private String name; + private String backgroundColor; + private String textColor; + + @Builder + public IssueLabelResponseDto(Long id, String name, String backgroundColor, String textColor) { + this.id = id; + this.name = name; + this.backgroundColor = backgroundColor; + this.textColor = textColor; + } + + public static IssueLabelResponseDto from(LabelVo label) { + return IssueLabelResponseDto.builder() + .id(label.getId()) + .name(label.getName()) + .backgroundColor(label.getBackgroundColor()) + .textColor(label.getTextColor()) + .build(); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueMilestoneResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueMilestoneResponseDto.java new file mode 100644 index 000000000..170800e99 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueMilestoneResponseDto.java @@ -0,0 +1,24 @@ +package codesquad.issueTracker.issue.dto; + +import codesquad.issueTracker.milestone.vo.MilestoneVo; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueMilestoneResponseDto { + private Long id; + private String name; + + @Builder + public IssueMilestoneResponseDto(Long id, String name) { + this.id = id; + this.name = name; + } + + public static IssueMilestoneResponseDto from(MilestoneVo milestoneVo) { + return builder() + .id(milestoneVo.getId()) + .name(milestoneVo.getName()) + .build(); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueOptionResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueOptionResponseDto.java new file mode 100644 index 000000000..a9e87f14a --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueOptionResponseDto.java @@ -0,0 +1,39 @@ +package codesquad.issueTracker.issue.dto; + +import codesquad.issueTracker.issue.vo.IssueLabelVo; +import codesquad.issueTracker.issue.vo.IssueMilestoneVo; +import codesquad.issueTracker.issue.vo.AssigneeVo; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueOptionResponseDto { + + private List assignees; + private List labels; + private IssueMilestoneVo milestone; + + @Builder + public IssueOptionResponseDto(List assignees, List labels, IssueMilestoneVo milestone) { + this.assignees = assignees; + this.labels = labels; + this.milestone = milestone; + } + + public static IssueOptionResponseDto of(List assignees, List labels, IssueMilestoneVo milestone) { + if (milestone.getId() == null) { + return IssueOptionResponseDto.builder() + .assignees(assignees) + .labels(labels) + .build(); + } + + return IssueOptionResponseDto.builder() + .assignees(assignees) + .labels(labels) + .milestone(milestone) + .build(); + } + +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueResponseDto.java new file mode 100644 index 000000000..9344a599c --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueResponseDto.java @@ -0,0 +1,35 @@ +package codesquad.issueTracker.issue.dto; + +import codesquad.issueTracker.issue.domain.Issue; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueResponseDto { + + private Long id; + private String title; + private String content; + private LocalDateTime createdAt; + private boolean isClose; + + @Builder + public IssueResponseDto(Long id, String title, String content, LocalDateTime createdAt, boolean isClose) { + this.id = id; + this.title = title; + this.content = content; + this.createdAt = createdAt; + this.isClose = isClose; + } + + public static IssueResponseDto from(Issue issue) { + return IssueResponseDto.builder() + .id(issue.getId()) + .title(issue.getTitle()) + .content(issue.getContent()) + .createdAt(issue.getCreatedAt()) + .isClose(issue.getIsClosed()) + .build(); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueUserResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueUserResponseDto.java new file mode 100644 index 000000000..7399bad3b --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueUserResponseDto.java @@ -0,0 +1,27 @@ +package codesquad.issueTracker.issue.dto; + +import codesquad.issueTracker.user.domain.User; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueUserResponseDto { + private Long id; + private String name; + private String imageUrl; + + @Builder + public IssueUserResponseDto(Long id, String name, String imageUrl) { + this.id = id; + this.name = name; + this.imageUrl = imageUrl; + } + + public static IssueUserResponseDto from(User user) { + return IssueUserResponseDto.builder() + .id(user.getId()) + .name(user.getName()) + .imageUrl(user.getProfileImg()) + .build(); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java b/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java index f54861999..0ffc3f7d0 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java @@ -1,8 +1,14 @@ package codesquad.issueTracker.issue.repository; -import java.time.LocalDateTime; + +import codesquad.issueTracker.issue.vo.AssigneeVo; +import codesquad.issueTracker.issue.vo.IssueMilestoneVo; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.time.LocalDateTime; + + import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.JdbcTemplate; @@ -56,6 +62,59 @@ public Long insertAssignees(Long issueId, Long userId) { return keyHolder.getKey().longValue(); } + + public Optional findActiveIssueById(Long issueId) { + String sql = "SELECT * FROM issues WHERE id = :issueId AND is_deleted = false"; + return Optional.ofNullable( + DataAccessUtils.singleResult( + jdbcTemplate.query(sql, Map.of("issueId", issueId), issueRowMapper))); + } + + private final RowMapper issueRowMapper = ((rs, rowNum) -> Issue.builder() + .id(rs.getLong("id")) + .title(rs.getString("title")) + .content(rs.getString("content")) + .createdAt(rs.getTimestamp("created_at").toLocalDateTime()) + .isClosed(rs.isClosed()) + .build()); + + public Optional findById(Long issueId) { + String sql = "SELECT * FROM issues WHERE id = :issueId"; + return Optional.ofNullable( + DataAccessUtils.singleResult( + jdbcTemplate.query(sql, Map.of("issueId", issueId), issueRowMapper))); + } + + public List findAssigneesById(Long issueId) { + String sql = "select u.id, u.name, u.profile_img " + + "from assignees a " + + " join users u on a.user_id = u.id " + + " join issues i on a.issue_id = i.user_id " + + "where i.id = :issueId " + + "AND i.is_deleted = false"; + return jdbcTemplate.query(sql, Map.of("issueId",issueId), assigneeVoRowMapper); + } + + private final RowMapper assigneeVoRowMapper = ((rs, rowNum) -> AssigneeVo.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .imgUrl(rs.getString("profile_img")) + .build()); + + + public int findCountByStatusAndMilestone(boolean status, IssueMilestoneVo milestone) { + String sql = "select COUNT(i.id) as count " + + "from issues i " + + " join milestones m on m.id = i.milestone_id " + + "where m.id = :milestoneId " + + "and i.is_deleted = false " + + "and i.is_closed = :status"; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("milestoneId", milestone.getId()); + params.addValue("status", status); + return jdbcTemplate.queryForObject(sql, params, Integer.class); + } + public Optional findById(Long id) { String sql = "SELECT id, milestone_id, user_id, title, content, created_at, is_closed FROM issues WHERE id = :id AND is_deleted = 0"; return Optional.ofNullable( @@ -89,7 +148,6 @@ public Long updateContent(Long id, String modifiedContent) { .addValue("modifiedContent", modifiedContent); jdbcTemplate.update(sql, parameterSource); return id; - } public Long updateTitle(Long id, String modifiedTitle) { @@ -125,4 +183,5 @@ public Long updateMilestone(Long issueId, Long milestoneId) { jdbcTemplate.update(sql, parameterSource); return issueId; } + } diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java b/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java index 7b1f7a06a..379484be2 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java @@ -1,5 +1,18 @@ package codesquad.issueTracker.issue.service; +import codesquad.issueTracker.global.common.Status; +import codesquad.issueTracker.issue.dto.IssueOptionResponseDto; +import codesquad.issueTracker.issue.dto.IssueLabelResponseDto; +import codesquad.issueTracker.issue.dto.IssueMilestoneResponseDto; +import codesquad.issueTracker.issue.vo.IssueLabelVo; +import codesquad.issueTracker.issue.vo.IssueMilestoneVo; +import codesquad.issueTracker.issue.dto.IssueResponseDto; +import codesquad.issueTracker.issue.dto.IssueUserResponseDto; +import codesquad.issueTracker.issue.vo.AssigneeVo; +import codesquad.issueTracker.label.dto.LabelResponseDto; + +import codesquad.issueTracker.milestone.vo.MilestoneVo; +import codesquad.issueTracker.user.domain.User; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -8,6 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import codesquad.issueTracker.comment.service.CommentService; import codesquad.issueTracker.global.common.Status; import codesquad.issueTracker.global.exception.CustomException; @@ -37,7 +51,6 @@ public class IssueService { private final LabelService labelService; private final UserService userService; private final MilestoneService milestoneService; - private final CommentService commentService; @Transactional public Long save(IssueWriteRequestDto request, Long id) { @@ -76,6 +89,62 @@ private void duplicatedId(List list) { } } + + public List getIssueLabels() { + LabelResponseDto allLabels = labelService.findAll(); + return allLabels.getLabels().stream() + .map(IssueLabelResponseDto::from) + .collect(Collectors.toList()); + } + + public List getIssueMilestones() { + List milestones = milestoneService.findMilestonesByStatus(Status.OPEN.getStatus()); + return milestones.stream() + .map(IssueMilestoneResponseDto::from) + .collect(Collectors.toList()); + } + + public List getIssueUsers() { + List users = userService.getUsers(); + return users.stream() + .map(IssueUserResponseDto::from) + .collect(Collectors.toList()); + } + + public IssueResponseDto getIssueById(Long issueId) { + validateExistIssue(issueId); + Issue issue = validateActiveIssueById(issueId); + + return IssueResponseDto.from(issue); + } + + private void validateExistIssue(Long issueId) { + issueRepository.findById(issueId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_EXIST_ISSUE)); + } + + private Issue validateActiveIssueById(Long issueId) { + return issueRepository.findActiveIssueById(issueId) + .orElseThrow(() -> new CustomException(ErrorCode.ALREADY_DELETED_ISSUE)); + } + + public IssueOptionResponseDto getIssueOptions(Long issueId) { + validateExistIssue(issueId); + validateActiveIssueById(issueId); + + List assignees = issueRepository.findAssigneesById(issueId); + List labels = labelService.findByIssueId(issueId); + IssueMilestoneVo milestone = milestoneService.findByIssueId(issueId); + + if (milestone.getId() != null) { + int closeCount = issueRepository.findCountByStatusAndMilestone(Status.CLOSED.getStatus(), milestone); + int openCount = issueRepository.findCountByStatusAndMilestone(Status.OPEN.getStatus(), milestone); + return IssueOptionResponseDto.of(assignees, labels, milestone.getMilestoneWithRatio(openCount, closeCount)); + } + + return IssueOptionResponseDto.of(assignees, labels, milestone); + } + @Transactional public List modifyIssueStatus(ModifyIssueStatusRequestDto request) { List issueIds = request.getIssueIds(); @@ -86,10 +155,8 @@ public List modifyIssueStatus(ModifyIssueStatusRequestDto request) { .map(issueId -> validateExistIssue(issueId)) .map(existIssue -> issueRepository.modifyStatus(existIssue.getId(), status)) .collect(Collectors.toList()); - ; } return issueIds; - } @Transactional @@ -97,7 +164,6 @@ public Long modifyIssueStatusInDetail(Long id, ModifyIssueStatusRequestDto reque Boolean status = Status.from(request.getStatus()).getStatus(); validateExistIssue(id); return issueRepository.modifyStatus(id, status); - } private Issue validateExistIssue(Long issuesIds) { diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/vo/AssigneeVo.java b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/AssigneeVo.java new file mode 100644 index 000000000..f5c017d59 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/AssigneeVo.java @@ -0,0 +1,18 @@ +package codesquad.issueTracker.issue.vo; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class AssigneeVo { + private Long id; + private String name; + private String imgUrl; + + @Builder + public AssigneeVo(Long id, String name, String imgUrl) { + this.id = id; + this.name = name; + this.imgUrl = imgUrl; + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueLabelVo.java b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueLabelVo.java new file mode 100644 index 000000000..e9de06b55 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueLabelVo.java @@ -0,0 +1,31 @@ +package codesquad.issueTracker.issue.vo; + +import codesquad.issueTracker.label.domain.Label; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueLabelVo { + + private Long id; + private String name; + private String backgroundColor; + private String textColor; + + @Builder + public IssueLabelVo(Long id, String name, String backgroundColor, String textColor) { + this.id = id; + this.name = name; + this.backgroundColor = backgroundColor; + this.textColor = textColor; + } + + public static IssueLabelVo from(Label label) { + return IssueLabelVo.builder() + .id(label.getId()) + .name(label.getName()) + .backgroundColor(label.getBackgroundColor()) + .textColor(label.getTextColor()) + .build(); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueMilestoneVo.java b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueMilestoneVo.java new file mode 100644 index 000000000..ae17b526e --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/vo/IssueMilestoneVo.java @@ -0,0 +1,37 @@ +package codesquad.issueTracker.issue.vo; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class IssueMilestoneVo { + private Long id; + private String name; + private int completedRatio; + + @Builder + public IssueMilestoneVo(Long id, String name, int completedRatio) { + this.id = id; + this.name = name; + this.completedRatio = completedRatio; + } + + public static IssueMilestoneVo from(IssueMilestoneVo issueMilestone) { + return IssueMilestoneVo.builder() + .id(issueMilestone.getId()) + .name(issueMilestone.getName()) + .build(); + } + + public IssueMilestoneVo getMilestoneWithRatio(int openCount, int closeCount) { + return IssueMilestoneVo.builder() + .id(this.id) + .name(this.name) + .completedRatio(calculateRatio(openCount, closeCount)) + .build(); + } + + private int calculateRatio(int openCount, int closeCount) { + return closeCount * 100 / (openCount + closeCount) ; + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java b/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java index d13e74f01..220f71517 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java +++ b/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java @@ -98,6 +98,18 @@ public Optional