주석 수정
This commit is contained in:
@@ -9,41 +9,77 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [인증/인가 컨트롤러]
|
||||
*
|
||||
* JWT(Json Web Token) 기반의 Stateless 인증 처리를 담당하는 엔드포인트 집합입니다.
|
||||
* 표준적인 Access/Refresh Token 패턴을 사용하며, 보안 강화를 위해
|
||||
* Refresh Token Rotation(RTR) 전략을 적용하여 탈취된 토큰의 재사용을 방지합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
class AuthController(
|
||||
private val authService: AuthService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 신규 회원 가입을 요청합니다.
|
||||
*
|
||||
* 봇(Bot)이나 무분별한 가입을 방지하기 위해, 가입 요청 즉시 인증 메일을 발송합니다.
|
||||
* 이메일 인증이 완료(`isVerified = true`)되기 전까지는 로그인이 제한됩니다.
|
||||
*/
|
||||
@PostMapping("/signup")
|
||||
fun signup(@RequestBody @Valid request: SignupRequest): ResponseEntity<ApiResponse<Nothing>> {
|
||||
authService.signup(request)
|
||||
return ResponseEntity.ok(ApiResponse.success(message = "회원가입에 성공했습니다. 이메일 인증을 완료해주세요."))
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 인증 코드를 검증하여 계정을 활성화합니다.
|
||||
* Redis에 TTL(Time-To-Live)로 저장된 임시 코드와 사용자의 입력을 대조합니다.
|
||||
*/
|
||||
@PostMapping("/verify")
|
||||
fun verifyEmail(@RequestBody @Valid request: VerifyEmailRequest): ResponseEntity<ApiResponse<Nothing>> {
|
||||
authService.verifyEmail(request.email, request.code)
|
||||
return ResponseEntity.ok(ApiResponse.success(message = "이메일 인증이 완료되었습니다."))
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 자격 증명(Email/Password)을 검증하고 토큰 쌍을 발급합니다.
|
||||
* 인증 성공 시 Access Token과 Refresh Token이 모두 반환됩니다.
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
fun login(@RequestBody @Valid request: LoginRequest): ResponseEntity<ApiResponse<TokenDto>> {
|
||||
val tokenDto = authService.login(request)
|
||||
return ResponseEntity.ok(ApiResponse.success(tokenDto))
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token 만료 시, Refresh Token을 사용하여 토큰을 갱신합니다 (Silent Refresh).
|
||||
*
|
||||
* [보안 전략: Refresh Token Rotation]
|
||||
* 토큰 갱신 시 기존 Refresh Token은 폐기되고 새로운 Refresh Token이 발급됩니다.
|
||||
* 만약 이미 폐기된 토큰으로 재요청이 들어올 경우, 탈취된 것으로 간주하여 해당 유저의 모든 토큰을 무효화합니다.
|
||||
*/
|
||||
@PostMapping("/reissue")
|
||||
fun reissue(@RequestBody request: ReissueRequest): ResponseEntity<ApiResponse<TokenDto>> {
|
||||
val tokenDto = authService.reissue(request.accessToken, request.refreshToken)
|
||||
return ResponseEntity.ok(ApiResponse.success(tokenDto))
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 처리를 수행합니다.
|
||||
*
|
||||
* JWT 특성상 클라이언트가 토큰을 삭제하는 것이 기본이지만,
|
||||
* 서버 측에서도 Redis에 저장된 Refresh Token을 즉시 삭제(Evict)하여
|
||||
* 더 이상 해당 토큰으로 액세스 토큰을 재발급받지 못하도록 차단합니다.
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
fun logout(@AuthenticationPrincipal user: User): ResponseEntity<ApiResponse<Nothing>> {
|
||||
authService.logout(user.username) // user.username은 email입니다.
|
||||
authService.logout(user.username) // user.username은 SecurityContext에 저장된 email입니다.
|
||||
return ResponseEntity.ok(ApiResponse.success(message = "로그아웃 되었습니다."))
|
||||
}
|
||||
}
|
||||
|
||||
// DTO가 다른 곳에서 재사용 않아 응집도를 위해 같은 파일 내에 정의
|
||||
data class ReissueRequest(val accessToken: String, val refreshToken: String)
|
||||
@@ -8,12 +8,24 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* [카테고리 조회 API]
|
||||
*
|
||||
* 일반 사용자(Public)에게 노출되는 카테고리 관련 엔드포인트입니다.
|
||||
* 블로그의 탐색(Navigation) 기능을 담당하며, 데이터 변경이 없는 읽기 전용(Read-Only) 작업만을 수행합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/categories")
|
||||
class CategoryController(
|
||||
private val categoryService: CategoryService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 카테고리 전체 계층 구조를 조회합니다.
|
||||
*
|
||||
* 프론트엔드 사이드바나 헤더 메뉴 렌더링을 위해 설계되었으며,
|
||||
* 불필요한 네트워크 왕복(Round Trip)을 줄이기 위해 한 번의 요청으로 중첩된(Nested) 트리 형태의 전체 데이터를 반환합니다.
|
||||
*/
|
||||
@GetMapping
|
||||
fun getCategoryTree(): ResponseEntity<ApiResponse<List<CategoryResponse>>> {
|
||||
return ResponseEntity.ok(ApiResponse.success(categoryService.getCategoryTree()))
|
||||
|
||||
@@ -10,34 +10,58 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [일반 사용자용 댓글 API]
|
||||
*
|
||||
* 게시글에 대한 사용자 참여(Social Interaction)를 담당하는 컨트롤러입니다.
|
||||
* 사용자 경험(UX)을 고려하여, 회원가입 없이도 자유롭게 소통할 수 있도록
|
||||
* 회원(Member)과 비회원(Guest)의 접근을 동시에 허용하는 하이브리드 로직을 수행합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/comments")
|
||||
class CommentController(
|
||||
private val commentService: CommentService
|
||||
) {
|
||||
|
||||
// 댓글 목록 조회
|
||||
/**
|
||||
* 특정 게시글의 전체 댓글 목록을 조회합니다.
|
||||
*
|
||||
* 단순 리스트가 아닌, 대댓글(Nested Comments) 구조를 유지한 상태로 반환하여
|
||||
* 클라이언트가 별도의 재귀 로직 구현 없이 트리 형태로 즉시 렌더링할 수 있도록 지원합니다.
|
||||
*/
|
||||
@GetMapping
|
||||
fun getComments(@RequestParam postSlug: String): ResponseEntity<ApiResponse<List<CommentResponse>>> {
|
||||
return ResponseEntity.ok(ApiResponse.success(commentService.getComments(postSlug)))
|
||||
}
|
||||
|
||||
// 댓글 작성 (회원 or 비회원)
|
||||
/**
|
||||
* 댓글을 작성합니다 (회원/비회원 공용).
|
||||
*
|
||||
* 참여 장벽을 낮추기 위해 로그인 여부를 강제하지 않습니다.
|
||||
* Security Context의 User 객체가 null일 경우 비회원으로 간주하며,
|
||||
* 이 경우 RequestBody에 포함된 닉네임과 비밀번호를 사용하여 임시 신원을 생성합니다.
|
||||
*/
|
||||
@PostMapping
|
||||
fun createComment(
|
||||
@RequestBody request: CommentSaveRequest,
|
||||
@AuthenticationPrincipal user: User? // 비회원이면 null이 들어옴
|
||||
@AuthenticationPrincipal user: User? // 비회원 접근 시 null (Optional Principal)
|
||||
): ResponseEntity<ApiResponse<Long>> {
|
||||
val email = user?.username // null이면 비회원
|
||||
val email = user?.username // 인증된 사용자라면 email 추출
|
||||
val commentId = commentService.createComment(request, email)
|
||||
return ResponseEntity.ok(ApiResponse.success(commentId, "댓글이 등록되었습니다."))
|
||||
}
|
||||
|
||||
// 댓글 삭제
|
||||
/**
|
||||
* 댓글을 삭제합니다.
|
||||
*
|
||||
* 작성자 유형(회원/비회원)에 따라 검증 전략(Strategy)이 달라집니다.
|
||||
* - 회원: 현재 로그인한 사용자의 ID와 댓글 작성자 ID의 일치 여부를 검증
|
||||
* - 비회원: 댓글 작성 시 설정한 비밀번호(Guest Password)의 일치 여부를 검증
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteComment(
|
||||
@PathVariable id: Long,
|
||||
@RequestBody(required = false) request: CommentDeleteRequest?, // 비회원용 비밀번호 바디
|
||||
@RequestBody(required = false) request: CommentDeleteRequest?, // 비회원일 경우에만 바디가 필요함
|
||||
@AuthenticationPrincipal user: User?
|
||||
): ResponseEntity<ApiResponse<Nothing>> {
|
||||
val email = user?.username
|
||||
|
||||
@@ -11,31 +11,54 @@ import org.springframework.data.web.PageableDefault
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [일반 사용자용 게시글 조회 API]
|
||||
*
|
||||
* 블로그의 핵심 콘텐츠인 게시글(Post) 데이터를 제공하는 Public 컨트롤러입니다.
|
||||
* 방문자의 조회 요청을 처리하며, 검색 엔진 최적화(SEO)를 고려하여
|
||||
* 내부 식별자(ID)가 아닌 의미 있는 문자열(Slug) 기반의 URL 설계를 채택했습니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/posts")
|
||||
class PostController(
|
||||
private val postService: PostService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 게시글 목록을 조회하거나 조건에 맞춰 검색합니다.
|
||||
*
|
||||
* [통합 검색 엔드포인트]
|
||||
* 단순 목록 조회와 필터링(검색) 로직을 하나의 API로 통합하여 프론트엔드 구현을 단순화했습니다.
|
||||
* 필터 조건(keyword, category, tag) 유무에 따라 동적 쿼리(QueryDSL) 또는 기본 페이징 쿼리로 분기 처리됩니다.
|
||||
*
|
||||
* @param keyword 제목 또는 본문 검색어 (Optional)
|
||||
* @param category 카테고리 이름 (Optional, 프론트엔드 파라미터명: category)
|
||||
* @param tag 태그 이름 (Optional)
|
||||
*/
|
||||
@GetMapping
|
||||
fun getPosts(
|
||||
@RequestParam(required = false) keyword: String?,
|
||||
@RequestParam(required = false) category: String?, // 👈 프론트는 'category'로 보냄
|
||||
@RequestParam(required = false) category: String?,
|
||||
@RequestParam(required = false) tag: String?,
|
||||
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) pageable: Pageable
|
||||
): ResponseEntity<ApiResponse<Page<PostSummaryResponse>>> {
|
||||
|
||||
// 검색 조건이 하나라도 있으면 searchPosts 호출 (검색 + 카테고리 필터링)
|
||||
// 필터 조건이 하나라도 존재하면 동적 쿼리(Search) 실행, 없으면 기본 목록 조회(List) 수행
|
||||
return if (keyword != null || category != null || tag != null) {
|
||||
val posts = postService.searchPosts(keyword, category, tag, pageable)
|
||||
ResponseEntity.ok(ApiResponse.success(posts))
|
||||
} else {
|
||||
// 조건이 없으면 전체 목록 조회
|
||||
val posts = postService.getPosts(pageable)
|
||||
ResponseEntity.ok(ApiResponse.success(posts))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 정보를 조회합니다.
|
||||
*
|
||||
* URL에 ID(숫자) 대신 제목 기반의 Slug를 사용하여 가독성과 SEO 점수를 높입니다.
|
||||
* 상세 조회 성공 시, 서비스 레이어에서 조회수(View Count) 증가 트랜잭션이 함께 수행됩니다.
|
||||
*/
|
||||
@GetMapping("/{slug}")
|
||||
fun getPost(@PathVariable slug: String): ResponseEntity<ApiResponse<PostResponse>> {
|
||||
val post = postService.getPostBySlug(slug)
|
||||
|
||||
@@ -8,12 +8,26 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* [일반 사용자용 프로필 조회 API]
|
||||
*
|
||||
* 블로그 방문자들에게 운영자(Owner)의 정보를 제공하는 Public 컨트롤러입니다.
|
||||
* 수정 권한이 필요한 관리자 영역(AdminProfileController)과 분리하여,
|
||||
* 불필요한 인증 로직 없이 누구나 빠르게 조회할 수 있도록 설계된 읽기 전용(Read-Only) 엔드포인트입니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/profile")
|
||||
class ProfileController(
|
||||
private val blogProfileService: BlogProfileService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 블로그 운영자의 프로필 정보를 조회합니다.
|
||||
*
|
||||
* 단일 사용자 블로그(Single User Blog) 특성상 별도의 사용자 ID 파라미터 없이
|
||||
* 시스템에 설정된 유일한 프로필 데이터를 반환합니다.
|
||||
* (주로 메인 화면의 사이드바나 About 페이지 렌더링에 사용됩니다.)
|
||||
*/
|
||||
@GetMapping
|
||||
fun getProfile(): ResponseEntity<ApiResponse<ProfileResponse>> {
|
||||
return ResponseEntity.ok(ApiResponse.success(blogProfileService.getProfile()))
|
||||
|
||||
@@ -7,19 +7,37 @@ import me.wypark.blogbackend.domain.category.CategoryService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [관리자용 카테고리 관리 API]
|
||||
*
|
||||
* 블로그의 카테고리 계층 구조(Tree Structure)를 조작하는 컨트롤러입니다.
|
||||
* 단순한 CRUD 외에도 부모-자식 관계 설정 및 구조 변경(이동) 로직을 포함하고 있습니다.
|
||||
*
|
||||
* Note:
|
||||
* - 데이터 무결성을 위해 모든 변경 작업은 트랜잭션 범위 안에서 유효성 검증(순환 참조 방지 등) 후 수행됩니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/categories")
|
||||
class AdminCategoryController(
|
||||
private val categoryService: CategoryService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 신규 카테고리를 생성합니다.
|
||||
* parentId가 없을 경우 최상위(Root) 카테고리로 생성되며, 있을 경우 해당 노드의 자식으로 연결됩니다.
|
||||
*/
|
||||
@PostMapping
|
||||
fun createCategory(@RequestBody request: CategoryCreateRequest): ResponseEntity<ApiResponse<Long>> {
|
||||
val id = categoryService.createCategory(request)
|
||||
return ResponseEntity.ok(ApiResponse.success(id, "카테고리가 생성되었습니다."))
|
||||
}
|
||||
|
||||
// 👈 [추가] 카테고리 수정 (이름, 위치 이동)
|
||||
/**
|
||||
* 카테고리 정보(이름 및 계층 위치)를 수정합니다.
|
||||
*
|
||||
* 단순 이름 변경뿐만 아니라, 부모 카테고리를 변경하여 트리 구조 내에서 위치를 이동시키는 기능도 수행합니다.
|
||||
* 위치 이동 시 순환 참조(Cycle)가 발생하지 않도록 서비스 레이어에서 검증 로직이 수행됩니다.
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
fun updateCategory(
|
||||
@PathVariable id: Long,
|
||||
@@ -29,6 +47,13 @@ class AdminCategoryController(
|
||||
return ResponseEntity.ok(ApiResponse.success(message = "카테고리가 수정되었습니다."))
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리를 삭제합니다.
|
||||
*
|
||||
* [삭제 정책]
|
||||
* - 하위 카테고리(Children)는 재귀적으로 함께 삭제됩니다 (Cascade).
|
||||
* - 해당 카테고리에 속해있던 게시글(Post)들은 삭제되지 않고 '미분류(Category = NULL)' 상태로 변경되어 보존됩니다.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteCategory(@PathVariable id: Long): ResponseEntity<ApiResponse<Nothing>> {
|
||||
categoryService.deleteCategory(id)
|
||||
|
||||
@@ -11,19 +11,36 @@ import org.springframework.data.web.PageableDefault
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [관리자용 댓글 관리 API]
|
||||
*
|
||||
* 블로그 내 모든 댓글 활동을 모니터링하고 중재(Moderation)하는 컨트롤러입니다.
|
||||
* 개별 게시글 단위로 조회하는 일반 API와 달리, 시스템 전체의 댓글 흐름을 파악하는 데 초점이 맞춰져 있습니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/comments")
|
||||
class AdminCommentController(
|
||||
private val commentService: CommentService
|
||||
) {
|
||||
|
||||
// 관리자 권한으로 댓글 삭제
|
||||
/**
|
||||
* 부적절한 댓글을 강제로 삭제합니다 (Moderation).
|
||||
*
|
||||
* 일반 사용자의 삭제 요청과 달리 작성자 본인 확인 절차(Ownership Check)를 건너뛰고,
|
||||
* 관리자 권한으로 즉시 데이터를 제거합니다.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteComment(@PathVariable id: Long): ResponseEntity<ApiResponse<Nothing>> {
|
||||
commentService.deleteCommentByAdmin(id)
|
||||
return ResponseEntity.ok(ApiResponse.success(message = "관리자 권한으로 댓글을 삭제했습니다."))
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 대시보드용 전체 댓글 목록을 조회합니다.
|
||||
*
|
||||
* 특정 게시글에 종속되지 않고, 최근 작성된 순서대로 모든 댓글을 페이징하여 반환합니다.
|
||||
* 이를 통해 관리자는 스팸이나 악성 댓글 발생 여부를 한눈에 파악할 수 있습니다.
|
||||
*/
|
||||
@GetMapping
|
||||
fun getAllComments(
|
||||
@PageableDefault(size = 20, sort = ["id"], direction = Sort.Direction.DESC) pageable: Pageable
|
||||
|
||||
@@ -10,12 +10,30 @@ import org.springframework.web.bind.annotation.RequestPart
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
/**
|
||||
* [관리자용 이미지 업로드 API]
|
||||
*
|
||||
* 게시글 본문(Markdown) 삽입용 이미지나 프로필 사진 등, 블로그 운영에 필요한
|
||||
* 정적 리소스(Static Resources)를 처리하는 컨트롤러입니다.
|
||||
*
|
||||
* 스토리지 저장소(S3/MinIO)와의 직접적인 통신은 Service Layer에 위임하며,
|
||||
* 클라이언트에게는 업로드된 리소스의 접근 가능한 URL을 반환하여 즉시 렌더링 가능하도록 합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/images")
|
||||
class AdminImageController(
|
||||
private val imageService: ImageService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 이미지를 업로드하고 접근 가능한 URL을 반환합니다.
|
||||
*
|
||||
* 주로 에디터(Toast UI 등)에서 이미지 첨부 이벤트가 발생했을 때 비동기로 호출되며,
|
||||
* 업로드 성공 시 반환된 URL은 클라이언트 측에서 즉시 Markdown 문법()으로 변환되어 본문에 삽입됩니다.
|
||||
*
|
||||
* @param image 클라이언트가 전송한 바이너리 파일 (MultipartFile)
|
||||
* @return CDN 또는 스토리지의 접근 가능한 절대 경로 (URL)
|
||||
*/
|
||||
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
fun uploadImage(
|
||||
@RequestPart("image") image: MultipartFile
|
||||
|
||||
@@ -9,12 +9,25 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* [관리자용 게시글 관리 API]
|
||||
*
|
||||
* 블로그 콘텐츠(Post)의 전체 수명 주기(Lifecycle)를 관리하는 컨트롤러입니다.
|
||||
* 게시글의 작성, 수정, 삭제 기능을 제공하며, 이 과정에서 입력값 검증(@Valid)과
|
||||
* 데이터 무결성 유지를 위한 다양한 비즈니스 로직(Slug 생성, 태그 처리 등)을 조율합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/posts")
|
||||
class AdminPostController(
|
||||
private val postService: PostService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 신규 게시글을 작성 및 발행합니다.
|
||||
*
|
||||
* Security Context에서 현재 로그인한 관리자 정보를 추출하여 작성자(Author)로 매핑함으로써,
|
||||
* 클라이언트가 임의로 작성자를 변조하는 것을 방지합니다.
|
||||
*/
|
||||
@PostMapping
|
||||
fun createPost(
|
||||
@RequestBody @Valid request: PostSaveRequest,
|
||||
@@ -24,7 +37,12 @@ class AdminPostController(
|
||||
return ResponseEntity.ok(ApiResponse.success(postId, "게시글이 작성되었습니다."))
|
||||
}
|
||||
|
||||
// 👈 [추가] 게시글 수정 엔드포인트
|
||||
/**
|
||||
* 기존 게시글을 수정합니다.
|
||||
*
|
||||
* 제목이나 본문 변경뿐만 아니라, 카테고리 이동이나 태그 재설정과 같은 메타데이터 변경도 함께 처리합니다.
|
||||
* 수정 시 사용되지 않게 된 이미지를 정리하거나, URL(Slug) 변경에 따른 리다이렉트 고려 등이 서비스 레이어에서 처리됩니다.
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
fun updatePost(
|
||||
@PathVariable id: Long,
|
||||
@@ -34,6 +52,14 @@ class AdminPostController(
|
||||
return ResponseEntity.ok(ApiResponse.success(postId, "게시글이 수정되었습니다."))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글을 영구 삭제합니다.
|
||||
*
|
||||
* [리소스 정리 전략]
|
||||
* 단순히 DB 레코드(Row)만 삭제하는 것이 아니라, 해당 게시글 본문에 포함되었던
|
||||
* S3 업로드 이미지 파일들을 추적하여 함께 삭제(Cleanup)합니다.
|
||||
* 이를 통해 스토리지에 불필요한 고아 파일(Orphaned Files)이 누적되는 것을 방지하여 비용을 최적화합니다.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
fun deletePost(@PathVariable id: Long): ResponseEntity<ApiResponse<Nothing>> {
|
||||
postService.deletePost(id)
|
||||
|
||||
@@ -9,12 +9,26 @@ import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* [관리자용 프로필 설정 API]
|
||||
*
|
||||
* 블로그 운영자의 소개(Bio), 프로필 사진, 소셜 링크 등을 관리하는 컨트롤러입니다.
|
||||
* 개인 블로그 특성상 단일 사용자(Owner)에 대한 정보만 존재하므로,
|
||||
* 별도의 ID 파라미터 없이 싱글톤(Singleton) 리소스처럼 관리됩니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/profile")
|
||||
class AdminProfileController(
|
||||
private val blogProfileService: BlogProfileService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 블로그 프로필 정보를 수정합니다.
|
||||
*
|
||||
* 단순 텍스트 정보(이름, 소개) 수정뿐만 아니라,
|
||||
* 변경된 프로필 이미지 URL을 반영하고 기존 이미지를 정리하는 로직이 서비스 레이어에 포함되어 있습니다.
|
||||
* 초기 데이터가 없을 경우(First Run), 수정 요청 시 기본 프로필이 생성(Upsert)됩니다.
|
||||
*/
|
||||
@PutMapping
|
||||
fun updateProfile(@RequestBody request: ProfileUpdateRequest): ResponseEntity<ApiResponse<Nothing>> {
|
||||
blogProfileService.updateProfile(request)
|
||||
|
||||
Reference in New Issue
Block a user