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
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ public record ActionItemUpdateRequest(@NotNull
@Schema(description = "실행 목표 리스트")
List<ActionItemUpdateElementRequest> actionItems) {

@Getter
public static class ActionItemUpdateElementRequest {
@Schema(description = "실행 목표 id")
@NotNull
Long id;
public record ActionItemUpdateElementRequest(
@Schema(description = "실행 목표 id (신규 생성시 null)")
Long id,

@Schema(description = "변경된 실행 목표 내용")
@NotNull
String content;
}
String content
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,34 +231,51 @@ public MemberActionItemGetResponse getMemberActionItemList(Long currentMemberId)
//== 실행 목표 수정 ==//
@Transactional
public void updateActionItems(Long memberId, Long retrospectId, ActionItemUpdateRequest updateDto) {
// 실행 목표 가져오기
List<ActionItem> actionItems = actionItemRepository.findAllByRetrospectId(retrospectId);

// 리더인지 검증
// 1. 리더 및 권한 검증
Retrospect retrospect = retrospectRepository.findByIdOrThrow(retrospectId);
Space space = spaceRepository.findByIdOrThrow(retrospect.getSpaceId());
space.isLeaderSpace(memberId);

// 요청 리스트와 DB에 저장된 실행 목표 개수가 다를 때
if (updateDto.actionItems().size() != actionItems.size()) {
throw new ActionItemException(INVALID_ACTION_ITEM_LIST);
}


// O(1) 접근을 위해서 map으로 변경
Map<Long, ActionItem> actionItemMap = actionItems.stream().collect(Collectors.toMap(
ActionItem::getId,
actionItem -> actionItem
));

AtomicInteger order = new AtomicInteger(1);
for (ActionItemUpdateRequest.ActionItemUpdateElementRequest updateItem : updateDto.actionItems()) {
ActionItem actionItem = actionItemMap.getOrDefault(updateItem.getId(), null);
if (actionItem == null) {
throw new ActionItemException(INVALID_ACTION_ITEM_ID);
// 2. DB에 저장된 기존 실행 목표 가져오기
List<ActionItem> dbActionItems = actionItemRepository.findAllByRetrospectId(retrospectId);

// 3. 요청 데이터에서 ID 추출 (Update 대상 식별용)
Set<Long> requestIds = updateDto.actionItems().stream()
.map(ActionItemUpdateRequest.ActionItemUpdateElementRequest::id)
.filter(Objects::nonNull) // ID가 있는 것만 (신규 생성 제외)
.collect(Collectors.toSet());

// 4. [DELETE] 요청 리스트에 없는 DB 항목 삭제
// (DB에는 있는데 요청 ID 목록에는 포함되지 않은 것들을 찾아서 삭제)
List<ActionItem> itemsToDelete = dbActionItems.stream()
.filter(item -> !requestIds.contains(item.getId()))
.toList();
actionItemRepository.deleteAll(itemsToDelete);

// 5. [UPDATE & CREATE] 요청 리스트 순서대로 처리
// 빠른 접근을 위해 DB 데이터를 Map으로 변환
Map<Long, ActionItem> actionItemMap = dbActionItems.stream()
.collect(Collectors.toMap(ActionItem::getId, item -> item));

int order = 1;

for (ActionItemUpdateRequest.ActionItemUpdateElementRequest requestItem : updateDto.actionItems()) {
if (requestItem.id() != null && actionItemMap.containsKey(requestItem.id())) {
// 5-1. [UPDATE] 기존 아이템 내용 및 순서 갱신
ActionItem actionItem = actionItemMap.get(requestItem.id());
actionItem.updateContent(requestItem.content());
actionItem.updateActionItemOrder(order++);
} else {
// 5-2. [CREATE] ID가 없거나 DB에 없는 ID인 경우 신규 생성
ActionItem newActionItem = ActionItem.builder()
.retrospectId(retrospectId)
.spaceId(space.getId())
.memberId(memberId)
.content(requestItem.content())
.actionItemOrder(order++)
.build();
actionItemRepository.save(newActionItem);
}
actionItem.updateContent(updateItem.getContent());
actionItem.updateActionItemOrder(order.getAndIncrement());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.layer.domain.actionItem.service;

import java.util.Comparator;
import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.layer.domain.actionItem.controller.dto.request.ActionItemUpdateRequest;
import org.layer.domain.actionItem.entity.ActionItem;
import org.layer.domain.actionItem.repository.ActionItemRepository;
import org.layer.domain.fixture.MemberFixture;
import org.layer.domain.fixture.RetrospectFixture;
import org.layer.domain.fixture.SpaceFixture;
import org.layer.domain.member.entity.Member;
import org.layer.domain.member.repository.MemberRepository;
import org.layer.domain.retrospect.entity.AnalysisStatus;
import org.layer.domain.retrospect.entity.Retrospect;
import org.layer.domain.retrospect.entity.RetrospectStatus;
import org.layer.domain.retrospect.repository.RetrospectRepository;
import org.layer.domain.space.entity.MemberSpaceRelation;
import org.layer.domain.space.entity.Space;
import org.layer.domain.space.repository.MemberSpaceRelationRepository;
import org.layer.domain.space.repository.SpaceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class ActionItemServiceTest {

@Autowired
private ActionItemService actionItemService;

@Autowired
private ActionItemRepository actionItemRepository;

@Autowired
private RetrospectRepository retrospectRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private SpaceRepository spaceRepository;

@Autowired
private MemberSpaceRelationRepository memberSpaceRelationRepository;

@Nested
class 실행_목표_수정 {

@Test
@DisplayName("요청 리스트에 따라 기존 항목 삭제, 내용 수정, 신규 생성이 수행되고 순서가 재정렬된다.")
void updateActionItems_Sync_Test() {
// given
// 1. 리더 멤버 및 스페이스 생성
Member leader = memberRepository.save(MemberFixture.createFixture("social-leader"));
Space space = spaceRepository.save(SpaceFixture.createFixture(leader.getId(), 1L));
memberSpaceRelationRepository.save(new MemberSpaceRelation(leader.getId(), space));

// 2. 회고 생성
Retrospect retrospect = retrospectRepository.save(RetrospectFixture.createFixture(
space.getId(), RetrospectStatus.PROCEEDING, AnalysisStatus.NOT_STARTED, null
));

// 3. [DB 상태] 기존 실행 목표 3개 저장 (A, B, C)
ActionItem itemA = actionItemRepository.save(createActionItem(retrospect, space, leader, "목표 A", 1));
ActionItem itemB = actionItemRepository.save(createActionItem(retrospect, space, leader, "목표 B", 2));
ActionItem itemC = actionItemRepository.save(createActionItem(retrospect, space, leader, "목표 C", 3));

// 4. [요청 생성] A, C 삭제 / B 수정 / D 신규 생성
// 기대 결과 순서: 1. B(수정됨) -> 2. D(신규)
List<ActionItemUpdateRequest.ActionItemUpdateElementRequest> updateElements = List.of(
// B 수정 (ID 유지)
new ActionItemUpdateRequest.ActionItemUpdateElementRequest(itemB.getId(), "목표 B 수정"),
// D 생성 (ID null)
new ActionItemUpdateRequest.ActionItemUpdateElementRequest(null, "목표 D 신규")
);
ActionItemUpdateRequest request = new ActionItemUpdateRequest(updateElements);

// when
// (서비스 메서드 호출)
actionItemService.updateActionItems(leader.getId(), retrospect.getId(), request);

// then
List<ActionItem> results = actionItemRepository.findAllByRetrospectId(retrospect.getId());

// 1. 개수 검증 (A, C 삭제, B 유지, D 생성 -> 총 2개)
Assertions.assertThat(results).hasSize(2);

// 순서대로 정렬하여 검증
results.sort(Comparator.comparingInt(ActionItem::getActionItemOrder));
ActionItem firstItem = results.get(0);
ActionItem secondItem = results.get(1);

// 2. 첫 번째 아이템 검증 (B 수정 확인)
Assertions.assertThat(firstItem.getId()).isEqualTo(itemB.getId());
Assertions.assertThat(firstItem.getContent()).isEqualTo("목표 B 수정");
Assertions.assertThat(firstItem.getActionItemOrder()).isEqualTo(1);

// 3. 두 번째 아이템 검증 (D 생성 확인)
Assertions.assertThat(secondItem.getId()).isNotEqualTo(itemA.getId()); // A가 아님
Assertions.assertThat(secondItem.getId()).isNotEqualTo(itemC.getId()); // C가 아님
Assertions.assertThat(secondItem.getContent()).isEqualTo("목표 D 신규");
Assertions.assertThat(secondItem.getActionItemOrder()).isEqualTo(2);
}

// 테스트용 헬퍼 메서드 (Fixture가 없다면 간편하게 사용)
private ActionItem createActionItem(Retrospect retrospect, Space space, Member member, String content, int order) {
return ActionItem.builder()
.retrospectId(retrospect.getId())
.spaceId(space.getId())
.memberId(member.getId())
.content(content)
.actionItemOrder(order)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ public AnalyzeDetail getTopCountAnalyzeDetailBy(AnalyzeDetailType analyzeDetailT
.orElse(getEmptyAnalyzeDetail());
}

public List<AnalyzeDetail> getAnalyzeDetailsBy(AnalyzeDetailType analyzeDetailType){
List<AnalyzeDetail> filteredDetails = new ArrayList<>();
for(AnalyzeDetail detail : analyzeDetails){
if(detail.getAnalyzeDetailType().equals(analyzeDetailType)){
filteredDetails.add(detail);
}
}
return filteredDetails;
}

private AnalyzeDetail getEmptyAnalyzeDetail(){
return AnalyzeDetail.builder().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.List;
import java.util.UUID;

import org.layer.domain.actionItem.entity.ActionItem;
import org.layer.domain.actionItem.repository.ActionItemRepository;
import org.layer.domain.analyze.entity.Analyze;
import org.layer.domain.analyze.entity.AnalyzeDetail;
import org.layer.domain.analyze.enums.AnalyzeDetailType;
Expand All @@ -21,9 +23,11 @@
import org.layer.domain.retrospect.entity.AnalysisStatus;
import org.layer.domain.retrospect.entity.Retrospect;
import org.layer.domain.retrospect.repository.RetrospectRepository;
import org.layer.domain.space.entity.Space;
import org.layer.domain.space.entity.Team;
import org.layer.domain.space.repository.MemberSpaceRelationRepository;
import org.layer.ai.dto.response.OpenAIResponse;
import org.layer.domain.space.repository.SpaceRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
Expand All @@ -39,11 +43,13 @@ public class AIAnalyzeService {
private static final int FIRST_RANK = 1;
private static final String RETROSPECT_LOCK_KEY = "retrospect:lock:";

private final SpaceRepository spaceRepository;
private final RetrospectRepository retrospectRepository;
private final AnalyzeRepository analyzeRepository;
private final QuestionRepository questionRepository;
private final AnswerRepository answerRepository;
private final MemberSpaceRelationRepository memberSpaceRelationRepository;
private final ActionItemRepository actionItemRepository;

private final OpenAIService openAIService;

Expand Down Expand Up @@ -74,8 +80,10 @@ public void createAnalyze(Long retrospectId) {
retrospect.validateAnalysisStatusIsNotDone();

// 답변 조회
Questions questions = new Questions(questionRepository.findAllByRetrospectIdOrderByQuestionOrder(retrospectId));
Answers answers = new Answers(answerRepository.findAllByRetrospectIdAndAnswerStatus(retrospectId, AnswerStatus.DONE));
Questions questions = new Questions(
questionRepository.findAllByRetrospectIdOrderByQuestionOrder(retrospectId));
Answers answers = new Answers(
answerRepository.findAllByRetrospectIdAndAnswerStatus(retrospectId, AnswerStatus.DONE));

Long rangeQuestionId = questions.extractEssentialQuestionIdBy(QuestionType.RANGER);
Long numberQuestionId = questions.extractEssentialQuestionIdBy(QuestionType.NUMBER);
Expand All @@ -85,9 +93,20 @@ public void createAnalyze(Long retrospectId) {
OpenAIResponse aiResponse = openAIService.createAnalyze(totalAnswer);
OpenAIResponse.Content content = aiResponse.parseContent();

Analyze teamAnalyze = getAnalyzeEntity(retrospectId, answers, rangeQuestionId, numberQuestionId, content, null, AnalyzeType.TEAM);
Analyze teamAnalyze = getAnalyzeEntity(retrospectId, answers, rangeQuestionId, numberQuestionId, content,
null, AnalyzeType.TEAM);
analyzeRepository.save(teamAnalyze);


// 팀 실행 목표 생성
Space space = spaceRepository.findByIdOrThrow(retrospect.getSpaceId());

List<ActionItem> actionItems = createActionItemsFromAnalyzeDetails(
teamAnalyze.getAnalyzeDetailsBy(AnalyzeDetailType.IMPROVEMENT),
retrospect.getSpaceId(),
retrospect.getId(), space.getLeaderId());
actionItemRepository.saveAll(actionItems);

// 팀원 개인마다의 분석 요청
Team team = new Team(memberSpaceRelationRepository.findAllBySpaceId(retrospect.getSpaceId()));

Expand All @@ -96,7 +115,8 @@ public void createAnalyze(Long retrospectId) {
String individualAnswer = answers.getIndividualAnswer(rangeQuestionId, numberQuestionId, memberId);
OpenAIResponse aiIndividualResponse = openAIService.createAnalyze(individualAnswer);
OpenAIResponse.Content individualContent = aiIndividualResponse.parseContent();
return getAnalyzeEntity(retrospectId, answers, rangeQuestionId, numberQuestionId, individualContent, memberId, AnalyzeType.INDIVIDUAL);
return getAnalyzeEntity(retrospectId, answers, rangeQuestionId, numberQuestionId, individualContent,
memberId, AnalyzeType.INDIVIDUAL);
})
.toList();
analyzeRepository.saveAll(individualAnalyzes);
Expand Down Expand Up @@ -126,11 +146,31 @@ private Analyze getAnalyzeEntity(Long retrospectId, Answers answers, Long rangeQ
List<AnalyzeDetail> analyzeDetails = createAnalyzeDetails(content);

return createAnalyzeEntity(retrospectId, memberId, answers.getScoreCount(numberQuestionId, SCORE_ONE, memberId),
answers.getScoreCount(numberQuestionId, SCORE_TWO, memberId), answers.getScoreCount(numberQuestionId, SCORE_THREE, memberId),
answers.getScoreCount(numberQuestionId, SCORE_FOUR, memberId), answers.getScoreCount(numberQuestionId, SCORE_FIVE, memberId),
answers.getScoreCount(numberQuestionId, SCORE_TWO, memberId),
answers.getScoreCount(numberQuestionId, SCORE_THREE, memberId),
answers.getScoreCount(numberQuestionId, SCORE_FOUR, memberId),
answers.getScoreCount(numberQuestionId, SCORE_FIVE, memberId),
answers.getGoalCompletionRate(rangeQuestionId), analyzeType, analyzeDetails);
}

private List<ActionItem> createActionItemsFromAnalyzeDetails(List<AnalyzeDetail> analyzeDetails, Long spaceId,
Long retrospectId, Long memberId) {
List<ActionItem> actionItems = new ArrayList<>();
int order = 1;
for (AnalyzeDetail detail : analyzeDetails) {
ActionItem actionItem = ActionItem.builder()
.retrospectId(retrospectId)
.spaceId(spaceId)
.memberId(memberId)
.content(detail.getContent())
.actionItemOrder(order)
.build();
actionItems.add(actionItem);
order++;
}
return actionItems;
}

private List<AnalyzeDetail> createAnalyzeDetails(OpenAIResponse.Content content) {
List<AnalyzeDetail> analyzeDetails = new ArrayList<>();

Expand Down Expand Up @@ -162,7 +202,8 @@ private List<AnalyzeDetail> createAnalyzeDetail(List<OpenAIResponse.ContentDetai
return analyzeDetails;
}

private Analyze createAnalyzeEntity(Long retrospectId, Long memberId,int scoreOne, int scoreTwo, int scoreThree, int scoreFour,
private Analyze createAnalyzeEntity(Long retrospectId, Long memberId, int scoreOne, int scoreTwo, int scoreThree,
int scoreFour,
int scoreFive, int goalCompletionRate, AnalyzeType analyzeType, List<AnalyzeDetail> analyzeDetails) {

return Analyze.builder()
Expand Down
Loading