Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,77 @@
package com.promsearch.community.interfaces;

import com.promsearch.community.interfaces.docs.CommentControllerDocs;
import com.promsearch.community.interfaces.dto.CommentListResponse;
import com.promsearch.community.interfaces.dto.CommentReplyResponse;
import com.promsearch.community.interfaces.dto.CommentResponse;
import com.promsearch.community.interfaces.dto.CreateCommentRequest;
import com.promsearch.community.interfaces.dto.UpdateCommentRequest;
import com.promsearch.global.exception.NotImplementedException;
import com.promsearch.global.response.ApiResponse;
import com.promsearch.global.security.AuthenticatedUserPrincipal;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
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;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("/api/v1")
public class CommentController implements CommentControllerDocs {

@GetMapping("/prompts/{promptId}/comments")
@Override
public ApiResponse<CommentListResponse> getComments(
@PathVariable Long promptId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user
) {
throw new NotImplementedException("댓글 목록 조회 기능은 아직 구현되지 않았습니다.");
}

@PostMapping("/prompts/{promptId}/comments")
@Override
public ResponseEntity<ApiResponse<CommentResponse>> createComment(
@PathVariable Long promptId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user,
@Valid @RequestBody CreateCommentRequest request
) {
throw new NotImplementedException("댓글 작성 기능은 아직 구현되지 않았습니다.");
}

@PatchMapping("/comments/{commentId}")
@Override
public ApiResponse<CommentResponse> updateComment(
@PathVariable Long commentId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user,
@Valid @RequestBody UpdateCommentRequest request
) {
throw new NotImplementedException("댓글 수정 기능은 아직 구현되지 않았습니다.");
}

@DeleteMapping("/comments/{commentId}")
@Override
public ApiResponse<Void> deleteComment(
@PathVariable Long commentId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user
) {
throw new NotImplementedException("댓글 삭제 기능은 아직 구현되지 않았습니다.");
}

@PostMapping("/comments/{commentId}/replies")
@Override
public ResponseEntity<ApiResponse<CommentReplyResponse>> createReply(
@PathVariable Long commentId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user,
@Valid @RequestBody CreateCommentRequest request
) {
throw new NotImplementedException("대댓글 작성 기능은 아직 구현되지 않았습니다.");
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.promsearch.community.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "댓글 작성자 공개 정보")
public record CommentAuthorResponse(
@Schema(description = "작성자 ID", example = "8")
Long userId,

@Schema(description = "작성자 공개 이름", example = "홍길동")
String displayName,

@Schema(
description = "작성자 프로필 이미지 URL",
example = "https://cdn.promsearch.com/profiles/8.jpg",
nullable = true
)
String profileImageUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.promsearch.community.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

@Schema(description = "페이지네이션 없이 전체 댓글을 반환하는 목록 응답")
public record CommentListResponse(
@Schema(description = "작성 시간 내림차순으로 정렬된 최상위 댓글 목록")
List<CommentResponse> comments
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.promsearch.community.interfaces.dto;

import com.promsearch.community.domain.enums.CommentStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;

@Schema(description = "대댓글 응답")
public record CommentReplyResponse(
@Schema(description = "대댓글 ID", example = "102")
Long commentId,

@Schema(description = "부모 댓글 ID", example = "101")
Long parentCommentId,

@Schema(description = "작성자 정보")
CommentAuthorResponse author,

@Schema(description = "대댓글 내용", example = "저도 그렇게 생각합니다.")
String content,

@Schema(description = "대댓글 상태", example = "ACTIVE")
CommentStatus status,

@Schema(description = "현재 로그인 사용자가 작성한 대댓글인지 여부", example = "true")
boolean mine,

@Schema(description = "작성 시각", example = "2026-07-23T03:10:00Z")
Instant createdAt,

@Schema(description = "수정 시각", example = "2026-07-23T03:10:00Z")
Instant updatedAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.promsearch.community.interfaces.dto;

import com.promsearch.community.domain.enums.CommentStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;

@Schema(description = "댓글 및 대댓글 응답")
public record CommentResponse(
@Schema(description = "댓글 ID", example = "101")
Long commentId,

@Schema(description = "부모 댓글 ID. 최상위 댓글이면 null입니다.", example = "100", nullable = true)
Long parentCommentId,

@Schema(description = "작성자 정보")
CommentAuthorResponse author,

@Schema(description = "댓글 내용", example = "좋은 프롬프트네요.")
String content,

@Schema(description = "댓글 상태", example = "ACTIVE")
CommentStatus status,

@Schema(description = "현재 로그인 사용자가 작성한 댓글인지 여부", example = "true")
boolean mine,

@Schema(description = "작성 시각", example = "2026-07-23T01:30:00Z")
Instant createdAt,

@Schema(description = "수정 시각", example = "2026-07-23T01:30:00Z")
Instant updatedAt,

@Schema(description = "작성 시간 내림차순으로 정렬된 대댓글 목록")
List<CommentReplyResponse> replies
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.promsearch.community.interfaces.dto;

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

@Schema(description = "댓글 또는 대댓글 작성 요청")
public record CreateCommentRequest(
@Schema(description = "댓글 내용", example = "좋은 프롬프트네요.")
@NotBlank(message = "content must not be blank")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.promsearch.community.interfaces.dto;

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

@Schema(description = "댓글 수정 요청")
public record UpdateCommentRequest(
@Schema(description = "수정할 댓글 내용", example = "수정된 댓글 내용입니다.")
@NotBlank(message = "content must not be blank")
String content
) {
}
39 changes: 39 additions & 0 deletions src/main/java/com/promsearch/global/response/ApiErrorExamples.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.promsearch.global.response;

public final class ApiErrorExamples {

public static final String BAD_REQUEST = """
{
"success": false,
"code": "COMMON-400",
"message": "잘못된 요청입니다."
}
""";

public static final String UNAUTHORIZED = """
{
"success": false,
"code": "COMMON-401",
"message": "인증이 필요합니다."
}
""";

public static final String FORBIDDEN = """
{
"success": false,
"code": "COMMON-403",
"message": "허용되지 않는 요청입니다."
}
""";

public static final String NOT_FOUND = """
{
"success": false,
"code": "COMMON-404",
"message": "요청한 리소스를 찾을 수 없습니다."
}
""";

private ApiErrorExamples() {
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/promsearch/global/response/ApiErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.promsearch.global.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "공통 API 오류 응답")
public record ApiErrorResponse(
@Schema(description = "요청 성공 여부", example = "false")
boolean success,

@Schema(description = "오류 코드", example = "COMMON-4XX")
String code,

@Schema(description = "오류 메시지", example = "요청 처리 중 오류가 발생했습니다.")
String message,

@Schema(description = "오류 상세 정보. 상세 정보가 없으면 응답에서 생략됩니다.", nullable = true)
Object result
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.promsearch.prompt.interfaces;

import com.promsearch.global.exception.NotImplementedException;
import com.promsearch.global.response.ApiResponse;
import com.promsearch.global.security.AuthenticatedUserPrincipal;
import com.promsearch.prompt.interfaces.docs.PromptControllerDocs;
import com.promsearch.prompt.interfaces.dto.PromptDetailResponse;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("/api/v1/prompts")
public class PromptController implements PromptControllerDocs {

@GetMapping("/{promptId}")
@Override
public ApiResponse<PromptDetailResponse> getPromptDetail(
@PathVariable Long promptId,
@AuthenticationPrincipal AuthenticatedUserPrincipal user
) {
throw new NotImplementedException("프롬프트 상세 조회 기능은 아직 구현되지 않았습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.promsearch.prompt.interfaces.docs;

import com.promsearch.global.response.ApiResponse;
import com.promsearch.global.response.ApiErrorExamples;
import com.promsearch.global.response.ApiErrorResponse;
import com.promsearch.global.security.AuthenticatedUserPrincipal;
import com.promsearch.prompt.interfaces.dto.PromptDetailResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Positive;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;

@Tag(name = "Prompt | 프롬프트", description = "프롬프트 상세 조회 API")
public interface PromptControllerDocs {

@Operation(
summary = "[PROMPT-001] 프롬프트 상세 조회",
description = """
프롬프트 상세 정보를 조회합니다. 인증 토큰은 선택 사항입니다.
비회원에게는 promptBody를 빈 문자열로 반환합니다.
PREMIUM 미결제 회원에게는 원문 앞부분 10% 이내이면서 최대 200자만 반환합니다.
이미지 URL은 워터마크 결과물의 Presigned URL만 제공합니다.
"""
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "상세 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400",
description = "유효하지 않은 프롬프트 ID",
content = @Content(
schema = @Schema(implementation = ApiErrorResponse.class),
examples = @ExampleObject(value = ApiErrorExamples.BAD_REQUEST)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "프롬프트 없음 또는 조회 불가",
content = @Content(
schema = @Schema(implementation = ApiErrorResponse.class),
examples = @ExampleObject(value = ApiErrorExamples.NOT_FOUND)
)
)
})
ApiResponse<PromptDetailResponse> getPromptDetail(
@Parameter(description = "프롬프트 ID", example = "10", required = true)
@Positive(message = "promptId must be positive")
@PathVariable Long promptId,

@Parameter(hidden = true)
@AuthenticationPrincipal AuthenticatedUserPrincipal user
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.promsearch.prompt.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "프롬프트 본문 잠금 사유")
public enum PromptAccessReason {
ANONYMOUS,
PREMIUM
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.promsearch.prompt.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "요청 사용자 기준 프롬프트 본문 접근 상태")
public record PromptAccessResponse(
@Schema(description = "원문 전체가 잠겨 있는지 여부", example = "true")
boolean locked,

@Schema(
description = "잠금 사유. 원문 전체를 조회할 수 있으면 null입니다.",
example = "PREMIUM",
nullable = true
)
PromptAccessReason reason
) {
}
Loading
Loading