주석 수정

This commit is contained in:
2026-02-03 15:05:28 +09:00
parent 0c72a603b3
commit 5869b8fe14
47 changed files with 1383 additions and 251 deletions

View File

@@ -4,12 +4,26 @@ import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
// 회원가입 요청
/**
* [회원가입 요청 DTO]
*
* 사용자 등록을 위한 데이터 전송 객체입니다.
* Controller 진입 시점(@Valid)에서 입력값의 형식 검증을 수행하여,
* 비즈니스 로직(Service) 단계에서의 불필요한 연산을 방지합니다 (Fail-Fast 전략).
*/
data class SignupRequest(
@field:NotBlank(message = "이메일은 필수입니다.")
@field:Email(message = "올바른 이메일 형식이 아닙니다.")
val email: String,
/**
* 비밀번호 복잡도 정책: 최소 8자 ~ 최대 20자
*
* Note:
* 이 필드는 클라이언트로부터 평문(Plain Text)으로 전달되므로,
* 전송 구간 암호화(HTTPS/TLS)가 보장된 환경에서만 사용되어야 합니다.
* DB 저장 시에는 반드시 단방향 해시 함수(BCrypt 등)를 통해 암호화됩니다.
*/
@field:NotBlank(message = "비밀번호는 필수입니다.")
@field:Size(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하로 입력해주세요.")
val password: String,
@@ -19,7 +33,11 @@ data class SignupRequest(
val nickname: String
)
// 로그인 요청
/**
* [로그인 요청 DTO]
*
* JWT 토큰 발급을 위한 사용자 자격 증명(Credentials)을 전달받는 객체입니다.
*/
data class LoginRequest(
@field:NotBlank(message = "이메일을 입력해주세요.")
val email: String,
@@ -28,6 +46,12 @@ data class LoginRequest(
val password: String
)
/**
* [이메일 인증 확인 DTO]
*
* 회원가입 직후 발송된 OTP(One Time Password) 코드를 검증하기 위한 요청 객체입니다.
* 이메일 소유권 확인(Proof of Ownership)을 위해 사용됩니다.
*/
data class VerifyEmailRequest(
@field:NotBlank(message = "이메일을 입력해주세요")
val email: String,

View File

@@ -2,44 +2,73 @@ package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.category.Category
// [요청] 카테고리 생성
/**
* [카테고리 생성 요청 DTO]
*
* 새로운 카테고리 노드(Node)를 생성하기 위한 요청 객체입니다.
* 계층형 게시판 구조를 지원하기 위해 부모 카테고리 ID(parentId)를 선택적으로 받습니다.
*
* @property parentId null일 경우 최상위(Root) 레벨에 생성되며, 값이 있을 경우 해당 카테고리의 하위(Child)로 연결됩니다.
*/
data class CategoryCreateRequest(
val name: String,
val parentId: Long? = null // null이면 최상위(Root) 카테고리
val parentId: Long? = null
)
// [요청] 카테고리 수정 (이름 + 부모 이동)
/**
* [카테고리 수정 요청 DTO]
*
* 카테고리의 속성 변경(Rename)과 구조 변경(Move)을 동시에 처리하는 객체입니다.
*
* Note:
* 트리 구조 내에서의 노드 이동(Move)은 데이터베이스 부하가 발생할 수 있고
* 순환 참조(Cycle) 위험이 있으므로, 서비스 레이어에서 별도의 정합성 검증 로직을 거칩니다.
*/
data class CategoryUpdateRequest(
val name: String,
val parentId: Long? // null이면 최상위(Root)로 이동
val parentId: Long? // 변경할 부모 ID (null이면 최상위로 이동)
)
// [응답] 카테고리 트리 구조 (재귀)
/**
* [카테고리 응답 DTO - Tree Structure]
*
* 프론트엔드 네비게이션 바(Sidebar) 등에서 계층형 메뉴를 렌더링하기 위한 재귀적 구조의 객체입니다.
*
* [성능 고려사항]
* N+1 문제를 방지하기 위해, 엔티티 조회 시점에는 Fetch Join을 사용하거나
* Batch Size를 설정하여 쿼리를 최적화한 후, 메모리 상에서 이 DTO 구조로 변환하여 반환합니다.
*/
data class CategoryResponse(
val id: Long,
val name: String,
val children: List<CategoryResponse> // 자식
val children: List<CategoryResponse> // 자식 노드 리스트 (Recursive)
) {
companion object {
// Entity -> DTO 변환 (재귀 호출)
// 엔티티 그래프를 순회하며 DTO 트리로 변환
fun from(category: Category): CategoryResponse {
return CategoryResponse(
id = category.id!!,
name = category.name,
// 자식들을 DTO 변환하여 리스트에 담음
// 자식 카테고리들을 재귀적으로 DTO 변환하여 리스트에 매핑
children = category.children.map { from(it) }
)
}
}
}
// [Admin용 응답] 관리자 대시보드 목록용
/**
* [관리자용 댓글 모니터링 DTO - Flat List]
*
* 관리자 대시보드에서 최근 댓글 흐름을 파악하기 위한 객체입니다.
* 계층형 구조(Nested)가 필요한 일반 사용자 뷰와 달리, 관리 목적상 시간순 나열이 중요하므로
* 모든 댓글을 평탄화(Flatten)하여 게시글 정보와 함께 제공합니다.
*/
data class AdminCommentResponse(
val id: Long,
val content: String,
val author: String,
val postTitle: String, // 어떤 글인지 식별
val postSlug: String, // 클릭 시 해당 글로 이동용
val postTitle: String, // 문맥 파악을 위한 원본 게시글 제목
val postSlug: String, // 클릭 시 해당 게시글로 바로 이동(Deep Link)하기 위한 식별자
val createdAt: java.time.LocalDateTime
) {
companion object {

View File

@@ -3,20 +3,32 @@ package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.comment.Comment
import java.time.LocalDateTime
// [응답] 댓글 (계층형 구조)
/**
* [댓글 응답 DTO - Hierarchical]
*
* 게시글 상세 화면에서 댓글 목록을 렌더링하기 위한 데이터 객체입니다.
* 대댓글(Nested Comment)을 포함하는 재귀적 구조를 가지며, 프론트엔드에서의 추가 가공 없이
* 즉시 트리 형태로 렌더링할 수 있도록 설계되었습니다.
*/
data class CommentResponse(
val id: Long,
val content: String,
val author: String,
// UI에서 게시글 작성자의 댓글을 강조(Highlight)하기 위한 플래그
val isPostAuthor: Boolean,
// 회원일 경우 프로필 링크 연결 등을 위해 ID 제공 (비회원은 null)
val memberId: Long?,
val createdAt: LocalDateTime,
// 자식 댓글 리스트 (Recursive)
val children: List<CommentResponse>
) {
companion object {
fun from(comment: Comment): CommentResponse {
// 게시글 작성자 ID와 댓글 작성자(회원) ID가 같은지 비교
// comment.member는 비회원일 경우 null이므로 안전하게 처리됨
// 게시글 작성자 본인이 쓴 댓글인지 확인 (비회원은 member가 null이므로 항상 false)
val isAuthor = comment.member?.id == comment.post.member.id
return CommentResponse(
@@ -26,23 +38,37 @@ data class CommentResponse(
isPostAuthor = isAuthor,
memberId = comment.member?.id,
createdAt = comment.createdAt,
children = comment.children.map { from(it) }
children = comment.children.map { from(it) } // 재귀 호출로 트리 구성
)
}
}
}
// [요청] 댓글 작성
/**
* [댓글 작성 요청 DTO]
*
* 회원과 비회원(Guest) 모두가 사용하는 통합 요청 객체입니다.
*
* [검증 로직]
* - 회원: Security Context에서 유저 정보를 가져오므로 guest 필드는 무시됩니다.
* - 비회원: guestNickname과 guestPassword가 필수값으로 요구됩니다.
*/
data class CommentSaveRequest(
val postSlug: String,
val content: String,
val parentId: Long? = null, // 대댓글일 경우 부모 ID
val parentId: Long? = null, // 대댓글(Reply)일 경우 상위 댓글 ID
// 비회원 전용 필드 (회원은 null 가능)
// --- 비회원 전용 필드 (Anonymous User) ---
val guestNickname: String? = null,
val guestPassword: String? = null
val guestPassword: String? = null // 수정/삭제 권한 인증용 비밀번호 (DB 저장 시 암호화됨)
)
// [요청] 댓글 삭제 (비회원용 비밀번호 전달)
/**
* [댓글 삭제 요청 DTO]
*
* 비회원이 본인의 댓글을 삭제할 때 비밀번호 검증을 위해 사용됩니다.
* 회원의 경우 JWT 토큰으로 본인 확인이 가능하므로 이 DTO의 필드는 사용되지 않습니다.
*/
data class CommentDeleteRequest(
val guestPassword: String? = null
)

View File

@@ -3,7 +3,13 @@ package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.post.Post
import java.time.LocalDateTime
// [응답] 인접 게시글 정보 (이전글/다음글)
/**
* [인접 게시글 응답 DTO]
*
* 게시글 상세 화면 하단에 위치할 '이전 글 / 다음 글' 네비게이션 링크를 위한 객체입니다.
* 전체 데이터를 로딩하는 대신, 링크 생성에 필요한 최소한의 식별자(Slug)와 제목(Title)만 포함하여
* 페이로드 크기를 최적화했습니다.
*/
data class PostNeighborResponse(
val slug: String,
val title: String
@@ -18,7 +24,15 @@ data class PostNeighborResponse(
}
}
// [응답] 게시글 상세 정보
/**
* [게시글 상세 응답 DTO]
*
* 단일 게시글의 모든 정보(Full Content)를 클라이언트에게 전달하는 객체입니다.
*
* [설계 의도]
* - SEO: ID 대신 Slug를 사용하여 검색 엔진 친화적인 URL 구조 지원
* - UX: 별도의 추가 요청 없이 이전/다음 글 정보를 함께 반환하여 페이지 이동성(Navigability) 향상
*/
data class PostResponse(
val id: Long,
val title: String,
@@ -27,11 +41,11 @@ data class PostResponse(
val categoryName: String?,
val viewCount: Long,
val createdAt: LocalDateTime,
// 👈 [추가] 이전/다음 게시글 정보
// 현재 글을 기준으로 앞/뒤 글 정보 (없으면 null)
val prevPost: PostNeighborResponse?,
val nextPost: PostNeighborResponse?
) {
// Entity -> DTO 변환 편의 메서드
companion object {
fun from(post: Post, prevPost: Post? = null, nextPost: Post? = null): PostResponse {
return PostResponse(
@@ -49,7 +63,15 @@ data class PostResponse(
}
}
// [응답] 게시글 목록용 (본문 제외, 가볍게)
/**
* [게시글 목록 응답 DTO]
*
* 메인 화면이나 카테고리 목록에서 사용되는 경량화(Lightweight) 객체입니다.
*
* [최적화 전략]
* 다수의 아이템을 렌더링해야 하므로, 데이터 전송량(Network Overhead)을 줄이기 위해
* 무거운 본문(content)은 제외하거나 미리보기용으로 일부만 포함하도록 설계되었습니다.
*/
data class PostSummaryResponse(
val id: Long,
val title: String,
@@ -58,7 +80,7 @@ data class PostSummaryResponse(
val viewCount: Long,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
val content: String?
val content: String? // 목록에서는 본문 미리보기 용도로 사용 (혹은 null)
) {
companion object {
fun from(post: Post): PostSummaryResponse {
@@ -76,11 +98,18 @@ data class PostSummaryResponse(
}
}
// [요청] 게시글 작성/수정
/**
* [게시글 작성/수정 요청 DTO]
*
* 게시글의 생명주기(생성/수정)를 담당하는 통합 커맨드 객체입니다.
*
* - Slug: 클라이언트가 직접 지정하지 않으면(null), 서버에서 제목을 기반으로 자동 생성(Generate)합니다.
* - Content: Markdown 포맷의 원문 텍스트를 저장합니다.
*/
data class PostSaveRequest(
val title: String,
val content: String, // 마크다운 원문
val content: String,
val slug: String? = null,
val categoryId: Long? = null,
val tags: List<String> = emptyList() // 태그는 나중에 구현
val tags: List<String> = emptyList() // 태그는 서비스 레이어에서 별도 로직으로 매핑(Many-to-Many) 처리
)

View File

@@ -2,6 +2,15 @@ package me.wypark.blogbackend.api.dto
import me.wypark.blogbackend.domain.profile.BlogProfile
/**
* [프로필 응답 DTO]
*
* 블로그 운영자(Owner)의 공개 정보를 렌더링하기 위한 View Object입니다.
*
* [설계 의도]
* 데이터베이스 엔티티(Entity)를 직접 반환하지 않고 DTO로 변환하여,
* 내부 구현의 변경이 클라이언트(View)에 영향을 미치지 않도록 결합도(Coupling)를 낮췄습니다.
*/
data class ProfileResponse(
val name: String,
val bio: String,
@@ -10,6 +19,7 @@ data class ProfileResponse(
val email: String?
) {
companion object {
// Entity -> DTO 변환 (Static Factory Method)
fun from(profile: BlogProfile): ProfileResponse {
return ProfileResponse(
name = profile.name,
@@ -22,6 +32,15 @@ data class ProfileResponse(
}
}
/**
* [프로필 수정 요청 DTO]
*
* 관리자 대시보드에서 블로그 설정(운영자 정보)을 변경하기 위한 요청 객체입니다.
*
* [유효성 정책]
* - 이름(name)과 소개(bio)는 블로그의 정체성을 나타내는 필수 항목입니다.
* - 프로필 이미지나 소셜 링크 등은 선택적(Optional)으로 입력할 수 있도록 Nullable로 설계되었습니다.
*/
data class ProfileUpdateRequest(
val name: String,
val bio: String,

View File

@@ -1,5 +1,17 @@
package me.wypark.blogbackend.api.dto
/**
* [JWT 토큰 응답 DTO]
*
* 로그인 또는 토큰 재발급 성공 시 클라이언트에게 반환되는 인증 정보 객체입니다.
* RFC 6750 (Bearer Token Usage) 표준을 따르며, 클라이언트가 인증 헤더(Authorization)를
* 올바르게 구성할 수 있도록 필요한 메타데이터를 함께 제공합니다.
*
* @property grantType 인증 타입 (Default: "Bearer")
* @property accessToken 리소스 접근을 위한 단기 유효 토큰 (Stateless)
* @property refreshToken Access Token 갱신을 위한 장기 유효 토큰 (Rotation 전략 적용)
* @property accessTokenExpiresIn Access Token의 유효 기간(ms). 클라이언트가 만료 시점을 예측하여 미리 갱신 요청을 보낼 수 있도록 함.
*/
data class TokenDto(
val grantType: String = "Bearer",
val accessToken: String,