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
59 changes: 46 additions & 13 deletions docs/reaction-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@

---

## 이모지 코드 목록

서버-클라이언트는 아래 코드 값으로 소통하며, **이미지 리소스는 클라이언트에서 관리**합니다.

| 코드 | 설명 |
|---|---|
| `LEC01` | 대단해 |
| `LEC02` | 완벽해 |
| `LEC03` | 최고야 |
| `LEC04` | 역시 |
| `LEC05` | 고생했어 |
| `LEC06` | 기대중 |
| `LEC07` | 괜찮아 |
| `LEC08` | 성장했다 |
| `LEC09` | 화이팅 |
| `LEC10` | 할 수 있다 |

---

## 목차

1. [사용 가능한 모든 반응 조회](#1-사용-가능한-모든-반응-조회)
Expand All @@ -32,11 +51,13 @@ GET /api/reaction
"reactions": [
{
"id": 1,
"imgUrl": "https://example.com/reaction/thumbs-up.png"
"emojiCode": "LEC01",
"description": "대단해"
},
{
"id": 2,
"imgUrl": "https://example.com/reaction/heart.png"
"emojiCode": "LEC02",
"description": "완벽해"
}
]
}
Expand All @@ -46,7 +67,8 @@ GET /api/reaction
|---|---|---|
| `reactions` | `array` | 반응 목록 |
| `reactions[].id` | `number` | 반응 ID |
| `reactions[].imgUrl` | `string` | 반응 이미지 URL |
| `reactions[].emojiCode` | `string` | 이모지 코드 (예: `LEC01`) |
| `reactions[].description` | `string` | 이모지 설명 (예: `대단해`) |

---

Expand Down Expand Up @@ -78,11 +100,13 @@ GET /api/reaction/recent?limit=8
"reactions": [
{
"id": 3,
"imgUrl": "https://example.com/reaction/fire.png"
"emojiCode": "LEC03",
"description": "최고야"
},
{
"id": 1,
"imgUrl": "https://example.com/reaction/thumbs-up.png"
"emojiCode": "LEC01",
"description": "대단해"
}
]
}
Expand Down Expand Up @@ -112,14 +136,14 @@ POST /space/{spaceId}/retrospect/{retrospectId}/reaction

```json
{
"reactionId": 1,
"emojiCode": "LEC01",
"answerId": 5
}
```

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `reactionId` | `number` | Y | 사용할 반응 ID (반응 목록 조회 API에서 가져온 값) |
| `emojiCode` | `string` | Y | 사용할 이모지 코드 (예: `LEC01`) |
| `answerId` | `number` | Y | 반응을 달 답변 ID |

### Response
Expand All @@ -129,7 +153,7 @@ POST /space/{spaceId}/retrospect/{retrospectId}/reaction
| `201` | 반응 생성 성공 |
| `400` | 이미 해당 답변에 반응을 달았음 |
| `403` | 해당 스페이스 멤버가 아님 |
| `404` | 존재하지 않는 반응 ID |
| `404` | 존재하지 않는 이모지 코드 |

---

Expand Down Expand Up @@ -185,13 +209,19 @@ GET /space/{spaceId}/retrospect/{retrospectId}/reaction
"reactions": [
{
"retrospectReactionId": 1,
"reactionId": 2,
"memberId": 29
"emojiCode": "LEC01",
"description": "대단해",
"memberId": 29,
"memberName": "홍길동",
"memberProfileImgUrl": "https://example.com/profile.png"
},
{
"retrospectReactionId": 2,
"reactionId": 5,
"memberId": 31
"emojiCode": "LEC05",
"description": "고생했어",
"memberId": 31,
"memberName": "김철수",
"memberProfileImgUrl": "https://example.com/profile2.png"
}
]
},
Expand All @@ -209,8 +239,11 @@ GET /space/{spaceId}/retrospect/{retrospectId}/reaction
| `answerReactions[].answerId` | `number` | 답변 ID |
| `answerReactions[].reactions` | `array` | 해당 답변에 달린 반응 목록 (없으면 빈 배열) |
| `reactions[].retrospectReactionId` | `number` | 회고 반응 ID — **삭제 시 이 값을 사용** |
| `reactions[].reactionId` | `number` | 어떤 반응인지 (imgUrl 매핑에 사용) |
| `reactions[].emojiCode` | `string` | 이모지 코드 (예: `LEC01`) |
| `reactions[].description` | `string` | 이모지 설명 (예: `대단해`) |
| `reactions[].memberId` | `number` | 반응을 단 멤버 ID — **내 반응 여부 판단에 사용** |
| `reactions[].memberName` | `string` | 반응을 단 멤버 이름 |
| `reactions[].memberProfileImgUrl` | `string` | 반응을 단 멤버 프로필 이미지 URL |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ public void create(AnswerListCreateRequest request, Long spaceId, Long retrospec

Answers answers = new Answers(answerRepository.findAllByRetrospectId(retrospectId));

// 마지막 답변인 경우 -> ai 분석 실행
if (answers.getWriteCount(retrospectId) == team.getTeamMemberCount()) {
// 회고 생성 시점에 스냅샷된 targetMemberCount 기준으로 트리거 (멤버 변동과 무관)
// null인 경우(기존 데이터)는 현재 팀 멤버 수로 폴백
long target = retrospect.getTargetMemberCount() != null
? retrospect.getTargetMemberCount()
: team.getTeamMemberCount();
if (answers.getWriteCount(retrospectId) >= target) {
retrospect.completeRetrospectAndStartAnalysis(time.now());
retrospectRepository.save(retrospect);
eventPublisher.publishEvent(AIAnalyzeStartEvent.of(retrospectId));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.layer.domain.reaction.controller.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@Schema(name = "RetrospectReactionCreateRequest", description = "회고 반응 생성 요청 DTO")
public record RetrospectReactionCreateRequest(
@NotNull
@Schema(description = "반응 ID", example = "1")
Long reactionId,
@NotBlank
@Schema(description = "이모지 코드", example = "LEC01")
String emojiCode,

@NotNull
@Schema(description = "답변 ID", example = "1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ public record ReactionGetResponse(
@Schema(description = "반응 ID", example = "1")
Long id,

@Schema(description = "반응 이미지 URL", example = "https://example.com/emoji.png")
String imgUrl
@Schema(description = "이모지 코드", example = "LEC01")
String emojiCode,

@Schema(description = "이모지 설명", example = "대단해")
String description
) {
public static ReactionGetResponse from(Reaction reaction) {
return new ReactionGetResponse(reaction.getId(), reaction.getImgUrl());
return new ReactionGetResponse(
reaction.getId(),
reaction.getEmojiCode().name(),
reaction.getEmojiCode().getDescription()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package org.layer.domain.reaction.controller.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import org.layer.domain.reaction.entity.EmojiCode;
import org.layer.domain.reaction.entity.RetrospectReaction;

@Schema(name = "RetrospectReactionElementResponse", description = "회고 반응 요소 응답 DTO")
public record RetrospectReactionElementResponse(
@Schema(description = "회고 반응 ID", example = "1")
Long retrospectReactionId,

@Schema(description = "반응 ID", example = "2")
Long reactionId,
@Schema(description = "이모지 코드", example = "LEC01")
String emojiCode,

@Schema(description = "이모지 설명", example = "대단해")
String description,

@Schema(description = "반응한 멤버 ID", example = "3")
Long memberId,
Expand All @@ -20,10 +24,16 @@ public record RetrospectReactionElementResponse(
@Schema(description = "반응한 멤버 프로필 이미지 URL", example = "https://example.com/profile.png")
String memberProfileImgUrl
) {
public static RetrospectReactionElementResponse of(RetrospectReaction retrospectReaction, String memberName, String memberProfileImgUrl) {
public static RetrospectReactionElementResponse of(
RetrospectReaction retrospectReaction,
EmojiCode emojiCode,
String memberName,
String memberProfileImgUrl
) {
return new RetrospectReactionElementResponse(
retrospectReaction.getId(),
retrospectReaction.getReactionId(),
emojiCode.name(),
emojiCode.getDescription(),
retrospectReaction.getMemberId(),
memberName,
memberProfileImgUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.layer.domain.reaction.controller.dto.response.ReactionListGetResponse;
import org.layer.domain.reaction.controller.dto.response.RetrospectReactionElementResponse;
import org.layer.domain.reaction.controller.dto.response.RetrospectReactionListGetResponse;
import org.layer.domain.reaction.entity.EmojiCode;
import org.layer.domain.reaction.entity.Reaction;
import org.layer.domain.reaction.entity.RetrospectReaction;
import org.layer.domain.reaction.exception.ReactionException;
Expand Down Expand Up @@ -48,14 +49,15 @@ public Long createReaction(Long spaceId, Long retrospectId, RetrospectReactionCr

retrospectRepository.findByIdOrThrow(retrospectId);

reactionRepository.findByIdOrThrow(request.reactionId());
EmojiCode emojiCode = EmojiCode.valueOf(request.emojiCode());
Reaction reaction = reactionRepository.findByEmojiCodeOrThrow(emojiCode);

if (retrospectReactionRepository.existsByAnswerIdAndMemberId(request.answerId(), memberId)) {
throw new ReactionException(ALREADY_REACTED);
}

RetrospectReaction retrospectReaction = RetrospectReaction.builder()
.reactionId(request.reactionId())
.reactionId(reaction.getId())
.answerId(request.answerId())
.memberId(memberId)
.build();
Expand Down Expand Up @@ -92,6 +94,10 @@ public RetrospectReactionListGetResponse getRetrospectReactions(Long spaceId, Lo

List<RetrospectReaction> retrospectReactions = retrospectReactionRepository.findAllByAnswerIdIn(answerIds);

List<Long> reactionIds = retrospectReactions.stream().map(RetrospectReaction::getReactionId).distinct().toList();
Map<Long, EmojiCode> reactionEmojiMap = reactionRepository.findAllById(reactionIds).stream()
.collect(Collectors.toMap(Reaction::getId, Reaction::getEmojiCode));

List<Long> memberIds = retrospectReactions.stream().map(RetrospectReaction::getMemberId).distinct().toList();
Members members = new Members(memberRepository.findAllById(memberIds));

Expand All @@ -105,6 +111,7 @@ public RetrospectReactionListGetResponse getRetrospectReactions(Long spaceId, Lo
.stream()
.map(r -> RetrospectReactionElementResponse.of(
r,
reactionEmojiMap.get(r.getReactionId()),
members.getName(r.getMemberId()),
members.getProfileImageUrl(r.getMemberId())
))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.layer.domain.retrospect.service;

import static org.layer.global.exception.RetrospectExceptionType.NO_ANSWERS_TO_CLOSE;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.layer.domain.retrospect.dto.SpaceMemberCount;
import org.layer.domain.space.entity.MemberSpaceRelation;
import org.layer.event.ai.AIAnalyzeStartEvent;
import org.layer.domain.answer.entity.Answers;
import org.layer.domain.answer.enums.AnswerStatus;
import org.layer.domain.answer.repository.AnswerRepository;
import org.layer.domain.retrospect.exception.RetrospectException;
import org.layer.domain.common.random.CustomRandom;
import org.layer.domain.common.time.Time;
import org.layer.domain.form.entity.Form;
Expand Down Expand Up @@ -65,7 +69,7 @@ public Long createRetrospect(RetrospectCreateRequest request, Long spaceId, Long
Team team = new Team(memberSpaceRelationRepository.findAllBySpaceId(spaceId));
team.validateTeamMembership(memberId);

Retrospect retrospect = getRetrospect(request, spaceId);
Retrospect retrospect = getRetrospect(request, spaceId, (int) team.getTeamMemberCount());
Retrospect savedRetrospect = retrospectRepository.save(retrospect);

List<Question> questions = getQuestions(request.questions(), savedRetrospect.getId(), null);
Expand Down Expand Up @@ -100,14 +104,15 @@ public Long createRetrospect(RetrospectCreateRequest request, Long spaceId, Long
return savedRetrospect.getId();
}

private Retrospect getRetrospect(RetrospectCreateRequest request, Long spaceId) {
private Retrospect getRetrospect(RetrospectCreateRequest request, Long spaceId, int targetMemberCount) {
return Retrospect.builder()
.spaceId(spaceId)
.title(request.title())
.introduction(request.introduction())
.retrospectStatus(RetrospectStatus.PROCEEDING)
.analysisStatus(AnalysisStatus.NOT_STARTED)
.deadline(request.deadline())
.targetMemberCount(targetMemberCount)
.build();
}

Expand Down Expand Up @@ -135,11 +140,14 @@ public RetrospectListGetResponse getRetrospects(Long spaceId, Long memberId) {

List<RetrospectGetResponse> retrospectDtos = retrospects.stream()
.map(r -> {
long totalCount = team.getTeamMemberCount();
if (r.getRetrospectStatus().equals(RetrospectStatus.DONE)) {
// 회고가 종료된 경우, 해당 회고의 deadline 시점의 팀원 수를 totalCount로 설정한다.
// RetrospectStatus 가 DONE 으로 변경되면, deadline이 null 값이 될 수 없기 때문이다.
long totalCount;
if (r.getTargetMemberCount() != null) {
totalCount = r.getTargetMemberCount();
} else if (r.getRetrospectStatus().equals(RetrospectStatus.DONE)) {
// targetMemberCount가 없는 기존 데이터: deadline 시점 팀원 수로 폴백
totalCount = team.getTeamMemberCountBefore(r.getDeadline());
} else {
totalCount = team.getTeamMemberCount();
}

return RetrospectGetResponse.of(r.getSpaceId(), r.getId(), r.getTitle(), r.getIntroduction(),
Expand All @@ -165,14 +173,13 @@ public RetrospectListGetResponse getAllRetrospects(Long memberId) {

List<RetrospectGetResponse> retrospectDtos = retrospects.stream()
.map(r -> {
long writeCount = spaceMemberCountMap.get(r.getSpaceId());
if (r.getRetrospectStatus().equals(RetrospectStatus.DONE)) {
writeCount = answers.getWriteCount(r.getId());
}
long totalCount = r.getTargetMemberCount() != null
? r.getTargetMemberCount()
: spaceMemberCountMap.get(r.getSpaceId());

return RetrospectGetResponse.of(r.getSpaceId(), r.getId(), r.getTitle(), r.getIntroduction(),
answers.getWriteStatus(memberId, r.getId()), r.getRetrospectStatus(), r.getAnalysisStatus(),
answers.getWriteCount(r.getId()), writeCount, r.getCreatedAt(), r.getDeadline());
answers.getWriteCount(r.getId()), totalCount, r.getCreatedAt(), r.getDeadline());
})
.toList();

Expand Down Expand Up @@ -230,6 +237,12 @@ public void closeRetrospect(Long spaceId, Long retrospectId, Long memberId) {

Retrospect retrospect = retrospectRepository.findByIdOrThrow(retrospectId);

// 작성된 답변이 없으면 마감 불가
Answers answers = new Answers(answerRepository.findAllByRetrospectIdAndAnswerStatus(retrospectId, AnswerStatus.DONE));
if (answers.getAnswers().isEmpty()) {
throw new RetrospectException(NO_ANSWERS_TO_CLOSE);
}

retrospect.completeRetrospectAndStartAnalysis(time.now());
if (retrospect.getAnalysisStatus().equals(AnalysisStatus.DONE)) {
log.error("비정상적인 오류입니다.");
Expand Down
Loading
Loading