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
93 changes: 49 additions & 44 deletions src/main/java/com/dokdok/topic/api/TopicAnswerApi.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.dokdok.topic.api;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.topic.dto.request.TopicAnswerRequest;
import com.dokdok.topic.dto.request.TopicAnswerBulkSaveRequest;
import com.dokdok.topic.dto.request.TopicAnswerBulkSubmitRequest;
import com.dokdok.topic.dto.response.PreOpinionSaveResponse;
import com.dokdok.topic.dto.response.PreOpinionSubmitResponse;
import com.dokdok.topic.dto.response.TopicAnswerDetailResponse;
import com.dokdok.topic.dto.response.TopicAnswerResponse;
import com.dokdok.topic.dto.response.TopicAnswerSubmitResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
Expand All @@ -16,7 +17,6 @@
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -25,28 +25,30 @@
import org.springframework.web.bind.annotation.RequestMapping;

@Tag(name = "토픽 답변", description = "토픽 답변 관련 API")
@RequestMapping("/api/gatherings/{gatheringId}/meetings/{meetingId}/topics/{topicId}/answers")
@RequestMapping("/api/gatherings/{gatheringId}/meetings/{meetingId}/answers")
public interface TopicAnswerApi {

@Operation(
summary = "토픽 답변 저장 (developer: 양재웅)",
summary = "토픽 답변 일괄 저장 (developer: 양재웅)",
description = """
토픽 답변을 저장합니다.
토픽 답변과 책 평가를 일괄 저장합니다.
- 권한: 모임 구성원
- 제약: 동일 토픽에 대해 1회만 저장 가능
- 제약: 제출 완료된 답변은 수정 불가
""",
parameters = {
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "topicId", description = "토픽 식별자", in = ParameterIn.PATH, required = true)
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "201",
description = "토픽 답변 저장 성공",
description = "토픽 답변 일괄 저장 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = TopicAnswerResponse.class))
schema = @Schema(implementation = PreOpinionSaveResponse.class),
examples = @ExampleObject(value = """
{"code":"CREATED","message":"사전 의견이 저장되었습니다.","data":{"review":{"reviewId":1,"bookId":10,"userId":1,"rating":4.5,"keywords":[]},"answers":[{"topicId":1,"isSubmitted":false,"updatedAt":"2026-01-19T20:01:37.105545"}]}}
"""))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
Expand Down Expand Up @@ -75,31 +77,32 @@ public interface TopicAnswerApi {
""")))
})
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<TopicAnswerResponse>> createAnswer(
ResponseEntity<ApiResponse<PreOpinionSaveResponse>> createAnswer(
@PathVariable("gatheringId") Long gatheringId,
@PathVariable("meetingId") Long meetingId,
@PathVariable("topicId") Long topicId,
@Valid @RequestBody TopicAnswerRequest request
@Valid @RequestBody TopicAnswerBulkSaveRequest request
);

@Operation(
summary = "내 토픽 답변 조회 (developer: 양재웅)",
summary = "사전 의견 작성 화면 조회 (developer: 양재웅)",
description = """
현재 로그인 사용자의 토픽 답변을 조회합니다.
현재 로그인 사용자의 사전 의견 작성 화면 정보를 조회합니다.
- 권한: 모임 구성원
""",
parameters = {
@Parameter(name = "gathering_id", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meeting_id", description = "약속 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "topic_id", description = "토픽 식별자", in = ParameterIn.PATH, required = true)
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "내 토픽 답변 조회 성공",
description = "사전 의견 작성 화면 조회 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = TopicAnswerDetailResponse.class))
schema = @Schema(implementation = TopicAnswerDetailResponse.class),
examples = @ExampleObject(value = """
{"code":"SUCCESS","message":"사전 의견 작성 화면 조회를 성공했습니다.","data":{"book":{"bookId":10,"title":"아주 작은 습관의 힘","author":"제임스 클리어"},"review":{"reviewId":1,"bookId":10,"userId":1,"rating":4.5,"keywords":[]},"preOpinion":{"updatedAt":"2026-02-06T09:12:30","topics":[{"topicId":1,"topicTitle":"책의 주요 메시지","topicDescription":"이 책에서 전달하고자 하는 핵심 메시지는 무엇인가요?","topicType":"DISCUSSION","topicTypeLabel":"토론형","confirmOrder":1,"content":"이 책은 작은 행동의 반복이 인생을 바꾼다고 생각합니다."},{"topicId":2,"topicTitle":"인상 깊은 구절","topicDescription":"가장 인상 깊었던 문장을 공유해주세요.","topicType":"EMOTION","topicTypeLabel":"감정 공유형","confirmOrder":2,"content":null}]}}}
"""))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "모임 멤버가 아님",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
Expand All @@ -119,30 +122,31 @@ ResponseEntity<ApiResponse<TopicAnswerResponse>> createAnswer(
})
@GetMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<TopicAnswerDetailResponse>> findMyAnswer(
@PathVariable("gathering_id") Long gatheringId,
@PathVariable("meeting_id") Long meetingId,
@PathVariable("topic_id") Long topicId
@PathVariable("gatheringId") Long gatheringId,
@PathVariable("meetingId") Long meetingId
);

@Operation(
summary = "토픽 답변 수정 (developer: 양재웅)",
summary = "토픽 답변 일괄 저장 (developer: 양재웅)",
description = """
현재 로그인 사용자의 토픽 답변을 수정합니다.
현재 로그인 사용자의 토픽 답변과 책 평가를 일괄 저장합니다.
- 권한: 모임 구성원
- 제약: 제출 완료된 답변은 수정 불가
""",
parameters = {
@Parameter(name = "gathering_id", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meeting_id", description = "약속 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "topic_id", description = "토픽 식별자", in = ParameterIn.PATH, required = true)
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "토픽 답변 수정 성공",
description = "토픽 답변 일괄 저장 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = TopicAnswerResponse.class))
schema = @Schema(implementation = PreOpinionSaveResponse.class),
examples = @ExampleObject(value = """
{"code":"UPDATED","message":"사전 의견이 저장되었습니다.","data":{"review":{"reviewId":1,"bookId":10,"userId":1,"rating":4.5,"keywords":[]},"answers":[{"topicId":1,"isSubmitted":false,"updatedAt":"2026-01-19T20:01:37.105545"}]}}
"""))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
Expand Down Expand Up @@ -171,32 +175,33 @@ ResponseEntity<ApiResponse<TopicAnswerDetailResponse>> findMyAnswer(
""")))
})
@PatchMapping(value = "/me", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<TopicAnswerResponse>> updateMyAnswer(
ResponseEntity<ApiResponse<PreOpinionSaveResponse>> updateMyAnswer(
@PathVariable("gatheringId") Long gatheringId,
@PathVariable("meetingId") Long meetingId,
@PathVariable("topicId") Long topicId,
@Valid @RequestBody TopicAnswerRequest request
@Valid @RequestBody TopicAnswerBulkSaveRequest request
);

@Operation(
summary = "토픽 답변 제출 (developer: 양재웅)",
summary = "토픽 답변 일괄 제출 (developer: 양재웅)",
description = """
현재 로그인 사용자의 토픽 답변을 제출합니다.
현재 로그인 사용자의 토픽 답변과 책 평가를 일괄 제출합니다.
- 권한: 모임 구성원
- 제약: 제출 완료된 답변은 재제출 불가
""",
parameters = {
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "topicId", description = "토픽 식별자", in = ParameterIn.PATH, required = true)
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "토픽 답변 제출 성공",
description = "토픽 답변 일괄 제출 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = TopicAnswerSubmitResponse.class))
schema = @Schema(implementation = PreOpinionSubmitResponse.class),
examples = @ExampleObject(value = """
{"code":"SUCCESS","message":"사전 의견이 제출되었습니다.","data":{"review":{"reviewId":1,"bookId":10,"userId":1,"rating":4.5,"keywords":[]},"answers":[{"topicId":1,"isSubmitted":true,"submittedAt":"2026-01-19T20:01:37.105545"}]}}
"""))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "모임 멤버가 아님",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
Expand All @@ -219,11 +224,11 @@ ResponseEntity<ApiResponse<TopicAnswerResponse>> updateMyAnswer(
{"code": "E000", "message": "서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.", "data": null}
""")))
})
@PatchMapping(value = "/submit", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<TopicAnswerSubmitResponse>> submitMyAnswer(
@PatchMapping(value = "/submit", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<PreOpinionSubmitResponse>> submitMyAnswer(
@PathVariable("gatheringId") Long gatheringId,
@PathVariable("meetingId") Long meetingId,
@PathVariable("topicId") Long topicId
@Valid @RequestBody TopicAnswerBulkSubmitRequest request
);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,73 @@

import com.dokdok.global.response.ApiResponse;
import com.dokdok.topic.api.TopicAnswerApi;
import com.dokdok.topic.dto.request.TopicAnswerRequest;
import com.dokdok.topic.dto.request.TopicAnswerBulkSaveRequest;
import com.dokdok.topic.dto.request.TopicAnswerBulkSubmitRequest;
import com.dokdok.topic.dto.response.PreOpinionSaveResponse;
import com.dokdok.topic.dto.response.PreOpinionSubmitResponse;
import com.dokdok.topic.dto.response.TopicAnswerDetailResponse;
import com.dokdok.topic.dto.response.TopicAnswerResponse;
import com.dokdok.topic.dto.response.TopicAnswerSubmitResponse;
import com.dokdok.topic.service.TopicAnswerService;
import lombok.RequiredArgsConstructor;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;


@RestController
@RequiredArgsConstructor
@RequestMapping("/api/gatherings/{gatheringId}/meetings/{meetingId}/topics/{topicId}/answers")
@RequestMapping("/api/gatherings/{gatheringId}/meetings/{meetingId}/answers")
public class TopicAnswerController implements TopicAnswerApi {

private final TopicAnswerService topicAnswerService;

@Override
public ResponseEntity<ApiResponse<TopicAnswerResponse>> createAnswer(
public ResponseEntity<ApiResponse<PreOpinionSaveResponse>> createAnswer(
Long gatheringId,
Long meetingId,
Long topicId,
TopicAnswerRequest request
@Valid @RequestBody TopicAnswerBulkSaveRequest request
) {
TopicAnswerResponse response = topicAnswerService.createAnswer(
gatheringId, meetingId, topicId, request
PreOpinionSaveResponse response = topicAnswerService.createAnswer(
gatheringId, meetingId, request
);

return ApiResponse.created(response, "답변이 저장되었습니다.");
return ApiResponse.created(response, "사전 의견이 저장되었습니다.");
}

@Override
public ResponseEntity<ApiResponse<TopicAnswerDetailResponse>> findMyAnswer(
Long gatheringId,
Long meetingId,
Long topicId
Long meetingId
) {
TopicAnswerDetailResponse response = topicAnswerService.getMyAnswer(
gatheringId, meetingId, topicId
gatheringId, meetingId
);

return ApiResponse.success(response, "조회 성공");
return ApiResponse.success(response, "사전 의견 작성 화면 조회를 성공했습니다.");
}

@Override
public ResponseEntity<ApiResponse<TopicAnswerResponse>> updateMyAnswer(
public ResponseEntity<ApiResponse<PreOpinionSaveResponse>> updateMyAnswer(
Long gatheringId,
Long meetingId,
Long topicId,
TopicAnswerRequest request
@Valid @RequestBody TopicAnswerBulkSaveRequest request
) {
TopicAnswerResponse response = topicAnswerService.updateMyAnswer(
gatheringId, meetingId, topicId, request
PreOpinionSaveResponse response = topicAnswerService.updateMyAnswer(
gatheringId, meetingId, request
);

return ApiResponse.updated(response, "답변이 수정되었습니다.");
return ApiResponse.updated(response, "사전 의견이 저장되었습니다.");
}

@Override
public ResponseEntity<ApiResponse<TopicAnswerSubmitResponse>> submitMyAnswer(
public ResponseEntity<ApiResponse<PreOpinionSubmitResponse>> submitMyAnswer(
Long gatheringId,
Long meetingId,
Long topicId
@Valid @RequestBody TopicAnswerBulkSubmitRequest request
) {
TopicAnswerSubmitResponse response = topicAnswerService.submitMyAnswer(
gatheringId, meetingId, topicId
PreOpinionSubmitResponse response = topicAnswerService.submitMyAnswer(
gatheringId, meetingId, request
);

return ApiResponse.success(response, "답변이 제출되었습니다.");
return ApiResponse.success(response, "사전 의견이 제출되었습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.dokdok.topic.dto.request;

import com.dokdok.book.dto.request.BookReviewRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.util.List;

@Schema(description = "사전 의견 일괄 저장 요청")
public record TopicAnswerBulkSaveRequest(
@NotNull
@Valid
@Schema(description = "책 평가 정보")
BookReviewRequest review,
@NotEmpty
@Valid
@Schema(description = "답변 목록")
List<AnswerItem> answers
) {
public record AnswerItem(
@NotNull
@Schema(description = "토픽 ID", example = "1")
Long topicId,
@Size(max = 1000, message = "설명은 1000자 이내여야 합니다")
@Schema(description = "답변 내용", example = "이 책은 작은 행동의 반복이 인생을 바꾼다고 생각합니다.", nullable = true)
String content
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dokdok.topic.dto.request;

import com.dokdok.book.dto.request.BookReviewRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.List;

@Schema(description = "사전 의견 일괄 제출 요청")
public record TopicAnswerBulkSubmitRequest(
@NotNull
@Valid
@Schema(description = "책 평가 정보")
BookReviewRequest review,
@NotEmpty
@Schema(description = "제출할 토픽 ID 목록", example = "[1,2,3]")
List<@NotNull Long> topicIds
) {
}
Loading