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
39 changes: 29 additions & 10 deletions src/main/java/com/issueDive/controller/LabelController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.issueDive.dto.CreateLabelRequest;
import com.issueDive.dto.LabelResponse;
import com.issueDive.dto.UpdateLabelRequest;
import com.issueDive.dto.IssueLabelsResponse;
import com.issueDive.service.LabelService;
import com.issueDive.service.IssueLabelService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -18,6 +20,7 @@
@RequiredArgsConstructor
public class LabelController {
private final LabelService labelService;
private final IssueLabelService issueLabelService;

//라벨 생성
@PostMapping("/labels")
Expand All @@ -40,27 +43,43 @@ public ResponseEntity<ApiResponse<List<LabelResponse>>> getLabels(){
}

//단일 라벨 조회
@GetMapping("/labels/{id}")
public ResponseEntity<ApiResponse<LabelResponse>> getLabelById(@PathVariable Long id){
LabelResponse data = labelService.getLabel(id);
@GetMapping("/labels/{labelId}")
public ResponseEntity<ApiResponse<LabelResponse>> getLabelById(@PathVariable Long labelId){
LabelResponse data = labelService.getLabel(labelId);

return ResponseEntity.ok(ApiResponse.ok(data));
}

//라벨 수정
@PatchMapping("/labels/{id}")
@PatchMapping("/labels/{labelId}")
public ResponseEntity<ApiResponse<LabelResponse>> updateLabel(
@PathVariable Long id,
@PathVariable Long labelId,
@Valid @RequestBody UpdateLabelRequest request){
LabelResponse data = labelService.updateLabel(id, request);
LabelResponse data = labelService.updateLabel(labelId, request);

return ResponseEntity.ok(ApiResponse.ok(data));
}

//라벨 삭제
@DeleteMapping("/labels/{id}")
public ResponseEntity<ApiResponse<Map<String, String>>> deleteLabel(@PathVariable Long id){
labelService.deleteLabel(id);
return ResponseEntity.ok(ApiResponse.ok(Map.of("message", "Label " + id + " deleted successfully")));
@DeleteMapping("/labels/{labelId}")
public ResponseEntity<ApiResponse<Map<String, String>>> deleteLabel(@PathVariable Long labelId){
labelService.deleteLabel(labelId);
return ResponseEntity.ok(ApiResponse.ok(Map.of("message", "Label " + labelId + " deleted successfully")));
}

@PostMapping("/issues/{issueId}/labels")
public ResponseEntity<ApiResponse<IssueLabelsResponse>> addLabels(
@PathVariable Long issueId,
@RequestBody List<Long> labelIds) {
IssueLabelsResponse data = issueLabelService.addLabelsToIssue(issueId, labelIds);
return ResponseEntity.ok(ApiResponse.ok(data));
}

@DeleteMapping("/issues/{issueId}/labels/{labelId}")
public ResponseEntity<ApiResponse<LabelResponse>> deleteLabelFromIssue(
@PathVariable Long issueId,
@PathVariable Long labelId) {
LabelResponse data = issueLabelService.deleteLabelFromIssue(issueId, labelId);
return ResponseEntity.ok(ApiResponse.ok(data));
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/issueDive/dto/IssueLabelsResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.issueDive.dto;


import com.issueDive.entity.Label;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Getter
@AllArgsConstructor
@Builder
public class IssueLabelsResponse {
private Long id;
private List<LabelSummary> labels;

@Getter
@AllArgsConstructor
@Builder
public static class LabelSummary {
private Long id;
private String name;
}

public static IssueLabelsResponse of(Long issueId, List<Label> labels) {
List<LabelSummary> summaries = new ArrayList<>();
for (Label label : labels) {
summaries.add(LabelSummary.builder()
.id(label.getId())
.name(label.getName())
.build());
}

return IssueLabelsResponse.builder()
.id(issueId)
.labels(summaries)
.build();
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/issueDive/entity/IssueLabel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.issueDive.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "issue_label")
public class IssueLabel {

@EmbeddedId
private IssueLabelId id;

@MapsId("issueId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "issue_id", nullable = false)
private Issue issue;

@MapsId("labelId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "label_id", nullable = false)
private Label label;

@Column(name = "added_at", nullable = false, updatable = false)
private LocalDateTime addedAt;

@PrePersist
public void prePersist() {
if (addedAt == null) {
addedAt = LocalDateTime.now();
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/issueDive/entity/IssueLabelId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.issueDive.entity;

import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Objects;

@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class IssueLabelId implements Serializable {
private Long issueId;
private Long labelId;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IssueLabelId that)) return false;
return Objects.equals(issueId, that.issueId) &&
Objects.equals(labelId, that.labelId);
}

@Override
public int hashCode() {
return Objects.hash(issueId, labelId);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/issueDive/entity/Label.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

import java.time.LocalDateTime;

@Entity
Expand All @@ -13,7 +14,7 @@
@Builder
@Table(name = "label")
public class Label {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ public ResponseEntity<ErrorResponse> handleBaseException(BaseException ex) {
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage()));
}

@ExceptionHandler(IssueLabelNotFoundException.class)
public ResponseEntity<ErrorResponse> handleIssueLabelNotFound(IssueLabelNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(ErrorCode.IssueLabelNotFound, e.getMessage()));
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
return ResponseEntity.badRequest()
.body(ErrorResponse.of(ErrorCode.ValidationError, "입력 값이 올바르지 않습니다."));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.issueDive.exception;

public class IssueLabelNotFoundException extends NotFoundException {
public IssueLabelNotFoundException(String message) {
super(message);
}
public IssueLabelNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/issueDive/repository/IssueLabelRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.issueDive.repository;

import com.issueDive.entity.Issue;
import com.issueDive.entity.IssueLabel;
import com.issueDive.entity.IssueLabelId;
import com.issueDive.entity.Label;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface IssueLabelRepository extends JpaRepository<IssueLabel, IssueLabelId> {
boolean existsByIssueAndLabel(Issue issue, Label label);
void deleteByIssueAndLabel(Issue issue, Label label);
List<IssueLabel> findAllByIssue(Issue issue);
void deleteByLabelId(Long labelId);
long countByLabel(Label label);
}
102 changes: 102 additions & 0 deletions src/main/java/com/issueDive/service/IssueLabelService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.issueDive.service;

import com.issueDive.dto.IssueLabelsResponse;
import com.issueDive.dto.LabelResponse;
import com.issueDive.entity.Issue;
import com.issueDive.entity.IssueLabel;
import com.issueDive.entity.IssueLabelId;
import com.issueDive.entity.Label;
import com.issueDive.exception.IssueLabelNotFoundException;
import com.issueDive.exception.LabelNotFoundException;
import com.issueDive.exception.NotFoundException;
import com.issueDive.repository.IssueLabelRepository;
import com.issueDive.repository.IssueRepository;
import com.issueDive.repository.LabelRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
@RequiredArgsConstructor
public class IssueLabelService {

private final IssueRepository issueRepository;
private final LabelRepository labelRepository;
private final IssueLabelRepository issueLabelRepository;

@Transactional
public IssueLabelsResponse addLabelsToIssue(Long issueId, List<Long> labelIds){
Issue issue = issueRepository.findById(issueId)
.orElseThrow(() -> new NotFoundException("Issue not found"));

List<Label> labels = labelRepository.findAllById(labelIds);


Set<Long> foundIds = new HashSet<>();
for (Label label : labels) {
foundIds.add(label.getId());
}

Long missingId = null;
for (Long id : labelIds) {
if (!foundIds.contains(id)) {
missingId = id;
break;
}
}
if (missingId != null) {
throw new LabelNotFoundException("Label not found: id=" + missingId);
}


for (Label label : labels) {
boolean alreadyExists = issueLabelRepository.existsByIssueAndLabel(issue, label);
if (!alreadyExists) {
IssueLabel issueLabel = IssueLabel.builder()
.id(new IssueLabelId(issue.getId(), label.getId()))
.issue(issue)
.label(label)
.build();
issueLabelRepository.save(issueLabel);
}
}

List<Label> currentLabels = getLabelsOfIssue(issue);

return IssueLabelsResponse.of(issueId, currentLabels);
}

@Transactional(readOnly = true)
public List<Label> getLabelsOfIssue(Issue issue){
List<IssueLabel> mappings = issueLabelRepository.findAllByIssue(issue);
List<Label> currentLabels = new ArrayList<>();
for (IssueLabel issueLabel : mappings) {
currentLabels.add(issueLabel.getLabel());
}
return currentLabels;
}

@Transactional
public LabelResponse deleteLabelFromIssue(Long issueId, Long labelId){
Issue issue = issueRepository.findById(issueId)
.orElseThrow(() -> new NotFoundException("Issue not found"));
Label label = labelRepository.findById(labelId)
.orElseThrow(() -> new NotFoundException("Label not found"));

boolean exists = issueLabelRepository.existsByIssueAndLabel(issue, label);

if (!exists){
throw new IssueLabelNotFoundException("Label " + labelId + "is not attached to Issue " + issueId);
}

issueLabelRepository.deleteByIssueAndLabel(issue, label);

return LabelResponse.from(label);
}

}
4 changes: 0 additions & 4 deletions src/main/java/com/issueDive/service/IssueService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
import com.issueDive.dto.IssueResponse;
import com.issueDive.dto.UpdateIssueRequest;
import com.issueDive.entity.*;
import com.issueDive.entity.Issue;
import com.issueDive.entity.IssueStatus;
import com.issueDive.entity.Label;
import com.issueDive.entity.User;
import com.issueDive.exception.ErrorCode;
import com.issueDive.exception.NotFoundException;
import com.issueDive.exception.ValidationException;
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/issueDive/service/LabelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.issueDive.exception.LabelNotFoundException;
import com.issueDive.exception.ValidationException;
import com.issueDive.repository.LabelRepository;
import com.issueDive.repository.IssueLabelRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -19,7 +20,7 @@
public class LabelService {

private final LabelRepository labelRepository;

private final IssueLabelRepository issueLabelRepository;
//라벨 생성
@Transactional
public LabelResponse createLabel(CreateLabelRequest request) {
Expand Down Expand Up @@ -90,6 +91,8 @@ public void deleteLabel(Long id) {
if (!labelRepository.existsById(id)) {
throw new LabelNotFoundException("Label not found: id=" + id);
}

issueLabelRepository.deleteByLabelId(id);
labelRepository.deleteById(id);
}
}