Repository: golden-dobakhe/Hakple
Pull request #37: 게시판 CRUD
Author: login: dark2138
Created: 2025-04-10T13:41:46Z | Merged: 2025-04-11T00:58:08Z

게시판 CRUD + 페이징 완료
게시판 검색 필터링 완료
게시판 태그 완료
게시판 신고 완료


Diff:
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/controller/ApiV1PostController.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/controller/ApiV1PostController.java
index c073afdf..6d33f60a 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/controller/ApiV1PostController.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/controller/ApiV1PostController.java
@@ -1,11 +1,142 @@
 package com.golden_dobakhe.HakPle.domain.post.post.controller;
 
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardRequest;
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardResponse;
+import com.golden_dobakhe.HakPle.domain.post.post.service.BoardService;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/api/v1/posts")
 public class ApiV1PostController {
-}
+
+    private final BoardService boardService;
+
+    @PostMapping
+    public ResponseEntity<BoardResponse> createBoard(
+            //@RequestHeader("X-User-Id") Long userId,
+            //@AuthenticationPrincipal CustomUserDetails userDetails,
+            @RequestBody BoardRequest request
+    ) {
+        //Long userId = userDetails.getMemberId();
+
+        // 하드코딩 테스트용
+        Long userId = 7L;
+        return ResponseEntity.ok(boardService.createBoard(request, userId));
+    }
+
+    @GetMapping("/{id}")
+    public ResponseEntity<BoardResponse> getBoard(@PathVariable Long id) {
+        return ResponseEntity.ok(boardService.getBoard(id));
+    }
+
+    @GetMapping
+    public ResponseEntity<Page<BoardResponse>> getBoards(
+            @RequestParam String academyCode,
+            @RequestParam(defaultValue = "1") int page,
+            @PageableDefault(size = 10) Pageable pageable) {
+
+        Sort sort = Sort.by(Sort.Direction.DESC, "creationTime");
+        Pageable adjustedPageable = PageRequest.of(page - 1, pageable.getPageSize(), sort);
+
+        return ResponseEntity.ok(boardService.getBoards(academyCode, adjustedPageable));
+    }
+
+    @GetMapping("/search")
+    public ResponseEntity<Page<BoardResponse>> searchBoards(
+            @RequestParam String academyCode,
+            @RequestParam String keyword,
+            @RequestParam(defaultValue = "1") int page,
+            @PageableDefault(size = 10) Pageable pageable) {
+
+        Sort sort = Sort.by(Sort.Direction.DESC, "creationTime");
+        Pageable adjustedPageable = PageRequest.of(page - 1, pageable.getPageSize(), sort);
+
+
+        return ResponseEntity.ok(boardService.searchBoards(academyCode, keyword, adjustedPageable));
+    }
+
+    @PutMapping("/{id}")
+    public ResponseEntity<BoardResponse> updateBoard(
+            //@RequestHeader("X-User-Id") Long userId,
+            //@AuthenticationPrincipal CustomUserDetails userDetails,
+            @PathVariable Long id,
+            @RequestBody BoardRequest request
+
+    ) {
+        //Long userId = userDetails.getMemberId();
+
+        // 하드코딩 테스트용
+        Long userId = 7L;
+        return ResponseEntity.ok(boardService.updateBoard(id, request, userId));
+    }
+
+    @DeleteMapping("/{id}")
+    public ResponseEntity<Void> deleteBoard(
+           // @RequestHeader("X-User-Id") Long userId,
+           // @AuthenticationPrincipal CustomUserDetails userDetails,
+            @PathVariable Long id
+
+    ) {
+       // Long userId = userDetails.getMemberId();
+
+        // 하드코딩 테스트용
+        Long userId = 7L;
+        boardService.deleteBoard(id, userId);
+        return ResponseEntity.ok().build();
+    }
+
+    @PostMapping("/{id}/like")
+    public ResponseEntity<Void> toggleLike(
+            // @RequestHeader("X-User-Id") Long userId,
+            // @AuthenticationPrincipal CustomUserDetails userDetails,
+            @PathVariable Long id
+    ) {
+
+        //Long userId = userDetails.getMemberId();
+
+        // 하드코딩 테스트용
+        Long userId = 7L;
+        boardService.toggleLike(id, userId);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/tag/{tag}")
+    public ResponseEntity<Page<BoardResponse>> getBoardsByTag(
+            @RequestParam String academyCode,
+            @PathVariable String tag,
+            @RequestParam(defaultValue = "1") int page,
+            @PageableDefault(size = 10) Pageable pageable
+
+    ) {
+
+        Sort sort = Sort.by(Sort.Direction.DESC, "creationTime");
+        Pageable adjustedPageable = PageRequest.of(page - 1, pageable.getPageSize(), sort);
+
+
+        return ResponseEntity.ok(boardService.getBoardsByTag(academyCode, tag, adjustedPageable));
+    }
+
+
+    @PostMapping("/{id}/report")
+    public ResponseEntity<Void> createBoardReport(
+            // @RequestHeader("X-User-Id") Long userId,
+            // @AuthenticationPrincipal CustomUserDetails userDetails,
+            @PathVariable Long id
+    ) {
+
+        //Long userId = userDetails.getMemberId();
+
+        // 하드코딩 테스트용
+        Long userId = 7L;
+        boardService.createBoardReport(id, userId);
+        return ResponseEntity.ok().build();
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardRequest.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardRequest.java
new file mode 100644
index 00000000..bbb1dc5b
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardRequest.java
@@ -0,0 +1,22 @@
+package com.golden_dobakhe.HakPle.domain.post.post.dto;
+
+import lombok.*;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+public class BoardRequest {
+    private String title;
+    private String content;
+    private String academyCode;
+    private List<String> tags;
+
+    @Builder
+    public BoardRequest(String title, String content, String academyCode, List<String> tags) {
+        this.title = title;
+        this.content = content;
+        this.academyCode = academyCode;
+        this.tags = tags;
+    }
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardResponse.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardResponse.java
new file mode 100644
index 00000000..a695276b
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/dto/BoardResponse.java
@@ -0,0 +1,38 @@
+package com.golden_dobakhe.HakPle.domain.post.post.dto;
+import com.golden_dobakhe.HakPle.domain.post.post.entity.Board;
+import com.golden_dobakhe.HakPle.global.entity.Status;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+@Getter
+public class BoardResponse {
+    private Long id;
+    private String title;
+    private String content;
+    private int viewCount;
+    private int likeCount;
+    private Status status;
+    private String academyCode;
+    private String userNickname;
+    private List<String> tags;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    @Builder
+    public BoardResponse(Board board, List<String> tags) {
+        this.id = board.getId();
+        this.title = board.getTitle();
+        this.content = board.getContent();
+        this.viewCount = board.getViewCount();
+        this.likeCount = board.getBoardLikes().size();
+        this.status = board.getStatus();
+        this.academyCode = board.getAcademyCode();
+        this.userNickname = board.getUser().getNickName();
+        this.tags = tags;
+        this.createdAt = board.getCreationTime();
+        this.updatedAt = board.getModificationTime();
+    }
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Board.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Board.java
index 5d5c36dd..1b022bcc 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Board.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Board.java
@@ -1,5 +1,6 @@
 package com.golden_dobakhe.HakPle.domain.post.post.entity;
 
+import com.golden_dobakhe.HakPle.domain.post.post.exception.BoardException;
 import com.golden_dobakhe.HakPle.domain.user.entity.User;
 import com.golden_dobakhe.HakPle.global.entity.BaseEntity;
 import com.golden_dobakhe.HakPle.global.entity.Status;
@@ -7,6 +8,9 @@
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @Getter
 @Setter
 @Entity
@@ -32,4 +36,40 @@ public class Board extends BaseEntity {
     @Enumerated(EnumType.STRING)
     @Column(nullable = false)
     private Status status; // ENUM('active', 'inactive', 'pending')
+
+    @Column(nullable = false)
+    private String academyCode; // 학원 코드
+
+    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
+    private List<BoardLike> boardLikes = new ArrayList<>(); //  좋아요 수
+
+
+    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
+    private List<TagMapping> tags = new ArrayList<>(); // 태그 매핑 리스트
+
+
+    public void increaseViewCount() {
+        this.viewCount++;
+    }
+
+    public void update(String title, String content) {
+        if (title != null && !title.isBlank()) {
+            this.title = title;
+        }
+        if (content != null && !content.isBlank()) {
+            this.content = content;
+        }
+    }
+
+    public void validateUser(Long userId) {
+        if (!this.user.getId().equals(userId)) {
+            throw BoardException.unauthorized();
+        }
+    }
+
+    public void validateStatus() {
+        if (this.status != Status.ACTIVE) {
+            throw BoardException.notFound();
+        }
+    }
 }
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/like/entity/BoardLike.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardLike.java
similarity index 75%
rename from backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/like/entity/BoardLike.java
rename to backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardLike.java
index cd4d3cdc..c26602f4 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/like/entity/BoardLike.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardLike.java
@@ -1,9 +1,10 @@
-package com.golden_dobakhe.HakPle.domain.post.like.entity;
+package com.golden_dobakhe.HakPle.domain.post.post.entity;
 
-import com.golden_dobakhe.HakPle.domain.post.post.entity.Board;
 import com.golden_dobakhe.HakPle.domain.user.entity.User;
 import com.golden_dobakhe.HakPle.global.entity.BaseEntity;
-import jakarta.persistence.*;
+import jakarta.persistence.Entity;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/report/entity/BoardReport.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardReport.java
similarity index 75%
rename from backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/report/entity/BoardReport.java
rename to backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardReport.java
index 450c1298..c57f906e 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/report/entity/BoardReport.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/BoardReport.java
@@ -1,9 +1,10 @@
-package com.golden_dobakhe.HakPle.domain.post.report.entity;
+package com.golden_dobakhe.HakPle.domain.post.post.entity;
 
-import com.golden_dobakhe.HakPle.domain.post.post.entity.Board;
 import com.golden_dobakhe.HakPle.domain.user.entity.User;
 import com.golden_dobakhe.HakPle.global.entity.BaseEntity;
-import jakarta.persistence.*;
+import jakarta.persistence.Entity;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Hashtag.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Hashtag.java
index 6b317883..bcf40993 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Hashtag.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/entity/Hashtag.java
@@ -15,4 +15,7 @@
 public class Hashtag extends BaseEntity {
     @Column(length=255, unique=true, nullable=false)
     String hashtagName; // 해시태그 이름 (유니크 설정됨)
+
+    @Column(nullable = false)
+    private String academyCode; // 학원 코드로 구분
 }
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/BoardException.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/BoardException.java
new file mode 100644
index 00000000..3dd87bca
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/BoardException.java
@@ -0,0 +1,31 @@
+package com.golden_dobakhe.HakPle.domain.post.post.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class BoardException extends RuntimeException {
+    private final HttpStatus status;
+    private final String message;
+
+    public BoardException(HttpStatus status, String message) {
+        this.status = status;
+        this.message = message;
+    }
+
+    public static BoardException notFound() {
+        return new BoardException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다.");
+    }
+
+    public static BoardException unauthorized() {
+        return new BoardException(HttpStatus.UNAUTHORIZED, "권한이 없습니다.");
+    }
+
+    public static BoardException invalidRequest() {
+        return new BoardException(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.");
+    }
+
+    public static BoardException duplicateTag() {
+        return new BoardException(HttpStatus.CONFLICT, "이미 존재하는 태그입니다.");
+    }
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..6ee41a05
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/exception/GlobalExceptionHandler.java
@@ -0,0 +1,25 @@
+package com.golden_dobakhe.HakPle.domain.post.post.exception;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BoardException.class)
+    public ResponseEntity<ErrorResponse> handleBoardException(BoardException e) {
+        return ResponseEntity
+                .status(e.getStatus())
+                .body(new ErrorResponse(e.getMessage()));
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> handleException(Exception e) {
+        return ResponseEntity
+                .status(500)
+                .body(new ErrorResponse("서버 오류가 발생했습니다."));
+    }
+
+    public record ErrorResponse(String message) {}
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardLikeRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardLikeRepository.java
new file mode 100644
index 00000000..3f86bde3
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardLikeRepository.java
@@ -0,0 +1,13 @@
+package com.golden_dobakhe.HakPle.domain.post.post.repository;
+
+import com.golden_dobakhe.HakPle.domain.post.post.entity.BoardLike;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+
+@Repository
+public interface BoardLikeRepository extends JpaRepository<BoardLike, Long> {
+    Optional<BoardLike> findByBoardIdAndUserId(Long boardId, Long userId);
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardReportRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardReportRepository.java
new file mode 100644
index 00000000..3601f78b
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardReportRepository.java
@@ -0,0 +1,12 @@
+package com.golden_dobakhe.HakPle.domain.post.post.repository;
+
+import com.golden_dobakhe.HakPle.domain.post.post.entity.BoardReport;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface BoardReportRepository extends JpaRepository<BoardReport, Long> {
+    Optional<BoardReport> findByBoardIdAndUserId(Long boardId, Long userId);
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardRepository.java
index 1bbbe493..6209f9e9 100644
--- a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardRepository.java
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/BoardRepository.java
@@ -1,12 +1,41 @@
 package com.golden_dobakhe.HakPle.domain.post.post.repository;
 
 import com.golden_dobakhe.HakPle.domain.post.post.entity.Board;
+import com.golden_dobakhe.HakPle.global.entity.Status;
 import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
 
 import java.util.List;
 
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
 @Repository
 public interface BoardRepository extends JpaRepository<Board, Long> {
-    List<Board> findByAcademyCode(String academyCode);
+    Page<Board> findByAcademyCodeAndStatus(String academyCode, Status status, Pageable pageable);
+
+    @Query("SELECT DISTINCT b FROM Board b " +
+            "LEFT JOIN b.tags t " +
+            "LEFT JOIN b.user u " +
+            "WHERE b.academyCode = :academyCode " +
+            "AND b.status = 'ACTIVE' " +
+            "AND (b.title LIKE %:keyword% " +
+            "OR b.content LIKE %:keyword% " +
+            "OR t.hashtag.hashtagName LIKE %:keyword% " +
+            "OR u.nickName LIKE %:keyword%)")
+    Page<Board> searchBoards(@Param("academyCode") String academyCode,
+                             @Param("keyword") String keyword,
+                             Pageable pageable);
+
+    @Query("SELECT DISTINCT b FROM Board b " +
+            "JOIN b.tags t " +
+            "WHERE b.academyCode = :academyCode " +
+            "AND b.status = 'ACTIVE' " +
+            "AND t.hashtag.hashtagName = :tag")
+    Page<Board> findByTagAndAcademyCode(@Param("academyCode") String academyCode,
+                                        @Param("tag") String tag,
+                                        Pageable pageable);
 }
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/HashtagRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/HashtagRepository.java
new file mode 100644
index 00000000..cb06da4f
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/HashtagRepository.java
@@ -0,0 +1,11 @@
+package com.golden_dobakhe.HakPle.domain.post.post.repository;
+
+import com.golden_dobakhe.HakPle.domain.post.post.entity.Hashtag;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+@Repository
+public interface HashtagRepository extends JpaRepository<Hashtag, Long> {
+    Optional<Hashtag> findByHashtagNameAndAcademyCode(String hashtagName, String academyCode);
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/TagMappingRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/TagMappingRepository.java
new file mode 100644
index 00000000..b4ba8fde
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/repository/TagMappingRepository.java
@@ -0,0 +1,11 @@
+package com.golden_dobakhe.HakPle.domain.post.post.repository;
+
+import com.golden_dobakhe.HakPle.domain.post.post.entity.Board;
+import com.golden_dobakhe.HakPle.domain.post.post.entity.TagMapping;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TagMappingRepository extends JpaRepository<TagMapping, Long> {
+    void deleteByBoard(Board board);
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/BoardService.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/BoardService.java
new file mode 100644
index 00000000..9060d032
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/BoardService.java
@@ -0,0 +1,19 @@
+package com.golden_dobakhe.HakPle.domain.post.post.service;
+
+
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardRequest;
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardResponse;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface BoardService {
+    BoardResponse createBoard(BoardRequest request, Long userId);
+    BoardResponse getBoard(Long id);
+    Page<BoardResponse> getBoards(String academyCode, Pageable pageable);
+    Page<BoardResponse> searchBoards(String academyCode, String keyword, Pageable pageable);
+    BoardResponse updateBoard(Long id, BoardRequest request, Long userId);
+    void deleteBoard(Long id, Long userId);
+    void toggleLike(Long boardId, Long userId);
+    Page<BoardResponse> getBoardsByTag(String academyCode, String tag, Pageable pageable);
+    void createBoardReport(Long boardId, Long userId);
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/impl/BoardServiceImpl.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/impl/BoardServiceImpl.java
new file mode 100644
index 00000000..75999a00
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/impl/BoardServiceImpl.java
@@ -0,0 +1,233 @@
+package com.golden_dobakhe.HakPle.domain.post.post.service.impl;
+
+
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardRequest;
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardResponse;
+import com.golden_dobakhe.HakPle.domain.post.post.entity.*;
+import com.golden_dobakhe.HakPle.domain.post.post.exception.BoardException;
+import com.golden_dobakhe.HakPle.domain.post.post.repository.*;
+import com.golden_dobakhe.HakPle.domain.post.post.service.BoardService;
+
+import com.golden_dobakhe.HakPle.domain.user.entity.User;
+import com.golden_dobakhe.HakPle.domain.user.repository.UserRepository;
+import com.golden_dobakhe.HakPle.global.entity.Status;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class BoardServiceImpl implements BoardService {
+
+    private final BoardRepository boardRepository;
+    private final UserRepository userRepository;
+    private final HashtagRepository hashtagRepository;
+    private final TagMappingRepository tagMappingRepository;
+    private final BoardLikeRepository boardLikeRepository;
+    private final BoardReportRepository boardReportRepository;
+
+    @Override
+    @Transactional
+    public BoardResponse createBoard(BoardRequest request, Long userId) {
+        validateBoardRequest(request);
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> BoardException.notFound());
+
+        Board board = Board.builder()
+                .title(request.getTitle())
+                .content(request.getContent())
+                .academyCode(request.getAcademyCode())
+                .user(user)
+                .status(Status.ACTIVE)
+                .build();
+
+        board = boardRepository.save(board);
+
+        if (request.getTags() != null) {
+            for (String tagName : request.getTags()) {
+                if (!StringUtils.hasText(tagName)) continue;
+
+                Hashtag hashtag = hashtagRepository.findByHashtagNameAndAcademyCode(tagName, request.getAcademyCode())
+                        .orElseGet(() -> hashtagRepository.save(Hashtag.builder()
+                                .hashtagName(tagName)
+                                .academyCode(request.getAcademyCode())
+                                .build()));
+
+                TagMapping tagMapping = TagMapping.builder()
+                        .board(board)
+                        .hashtag(hashtag)
+                        .build();
+
+                tagMappingRepository.save(tagMapping);
+            }
+        }
+
+        return createBoardResponse(board);
+    }
+
+    @Override
+    @Transactional(readOnly = true)
+    public BoardResponse getBoard(Long id) {
+        Board board = boardRepository.findById(id)
+                .orElseThrow(() -> BoardException.notFound());
+
+        board.validateStatus();
+        board.increaseViewCount();
+        boardRepository.save(board);
+
+        return createBoardResponse(board);
+    }
+
+    @Override
+    @Transactional(readOnly = true)
+    public Page<BoardResponse> getBoards(String academyCode, Pageable pageable) {
+        if (!StringUtils.hasText(academyCode)) {
+            throw BoardException.invalidRequest();
+        }
+        return boardRepository.findByAcademyCodeAndStatus(academyCode, Status.ACTIVE, pageable)
+                .map(this::createBoardResponse);
+    }
+
+    @Override
+    @Transactional(readOnly = true)
+    public Page<BoardResponse> searchBoards(String academyCode, String keyword, Pageable pageable) {
+        if (!StringUtils.hasText(academyCode) || !StringUtils.hasText(keyword)) {
+            throw BoardException.invalidRequest();
+        }
+        return boardRepository.searchBoards(academyCode, keyword, pageable)
+                .map(this::createBoardResponse);
+    }
+
+    @Override
+    @Transactional
+    public BoardResponse updateBoard(Long id, BoardRequest request, Long userId) {
+        validateBoardRequest(request);
+
+        Board board = boardRepository.findById(id)
+                .orElseThrow(() -> BoardException.notFound());
+
+        board.validateUser(userId);
+        board.validateStatus();
+        board.update(request.getTitle(), request.getContent());
+
+        tagMappingRepository.deleteByBoard(board);
+
+        if (request.getTags() != null) {
+            for (String tagName : request.getTags()) {
+                if (!StringUtils.hasText(tagName)) continue;
+
+                Hashtag hashtag = hashtagRepository.findByHashtagNameAndAcademyCode(tagName, request.getAcademyCode())
+                        .orElseGet(() -> hashtagRepository.save(Hashtag.builder()
+                                .hashtagName(tagName)
+                                .academyCode(request.getAcademyCode())
+                                .build()));
+
+                TagMapping tagMapping = TagMapping.builder()
+                        .board(board)
+                        .hashtag(hashtag)
+                        .build();
+
+                tagMappingRepository.save(tagMapping);
+            }
+        }
+
+        return createBoardResponse(board);
+    }
+
+    @Override
+    @Transactional
+    public void deleteBoard(Long id, Long userId) {
+        Board board = boardRepository.findById(id)
+                .orElseThrow(() -> BoardException.notFound());
+
+        board.validateUser(userId);
+        board.validateStatus();
+        board.setStatus(Status.INACTIVE);
+        boardRepository.save(board);
+    }
+
+    @Override
+    @Transactional
+    public void toggleLike(Long boardId, Long userId) {
+        Board board = boardRepository.findById(boardId)
+                .orElseThrow(() -> BoardException.notFound());
+
+        board.validateStatus();
+
+        boardLikeRepository.findByBoardIdAndUserId(boardId, userId)
+                .ifPresentOrElse(
+                        boardLikeRepository::delete,
+                        () -> {
+                            BoardLike boardLike = BoardLike.builder()
+                                    .board(board)
+                                    .user(userRepository.findById(userId)
+                                            .orElseThrow(() -> BoardException.notFound()))
+                                    .build();
+                            boardLikeRepository.save(boardLike);
+                        }
+                );
+    }
+
+    @Override
+    @Transactional(readOnly = true)
+    public Page<BoardResponse> getBoardsByTag(String academyCode, String tag, Pageable pageable) {
+        if (!StringUtils.hasText(academyCode) || !StringUtils.hasText(tag)) {
+            throw BoardException.invalidRequest();
+        }
+        return boardRepository.findByTagAndAcademyCode(academyCode, tag, pageable)
+                .map(this::createBoardResponse);
+    }
+
+    @Override
+    @Transactional
+    public void createBoardReport(Long boardId, Long userId) {
+        Board board = boardRepository.findById(boardId)
+                .orElseThrow(() -> BoardException.notFound());
+
+        board.validateStatus();
+
+        if (boardReportRepository.findByBoardIdAndUserId(boardId, userId).isPresent()) {
+            throw BoardException.invalidRequest();
+        }
+
+
+        BoardReport boardReport = BoardReport.builder()
+                .board(board)
+                .user(userRepository.findById(userId)
+                        .orElseThrow(() -> BoardException.notFound()))
+                .build();
+
+        boardReportRepository.save(boardReport);
+    }
+
+    private BoardResponse createBoardResponse(Board board) {
+        List<String> tags = board.getTags().stream()
+                .map(tagMapping -> tagMapping.getHashtag().getHashtagName())
+                .collect(Collectors.toList());
+
+        return BoardResponse.builder()
+                .board(board)
+                .tags(tags)
+                .build();
+    }
+
+    private void validateBoardRequest(BoardRequest request) {
+        if (!StringUtils.hasText(request.getTitle())) {
+            throw BoardException.invalidRequest();
+        }
+        if (!StringUtils.hasText(request.getContent())) {
+            throw BoardException.invalidRequest();
+        }
+        if (!StringUtils.hasText(request.getAcademyCode())) {
+            throw BoardException.invalidRequest();
+        }
+    }
+}
diff --git a/backend/src/main/java/com/golden_dobakhe/HakPle/domain/user/repository/UserRepository.java b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/user/repository/UserRepository.java
new file mode 100644
index 00000000..0d005393
--- /dev/null
+++ b/backend/src/main/java/com/golden_dobakhe/HakPle/domain/user/repository/UserRepository.java
@@ -0,0 +1,7 @@
+package com.golden_dobakhe.HakPle.domain.user.repository;
+
+import com.golden_dobakhe.HakPle.domain.user.entity.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserRepository extends JpaRepository<User, Long> {
+}


Reviews:
user: login: jiyuuuuun
body: Exception 만드신거 좋네요!
API 테스트 완료 하셨고 스웨거 추가하시면 머지하셔도 될 것 같습니다!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T23:54:59Z
user: login: JAWSP
body: 웜매 벌써 많이 하셨네요
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-11T00:47:35Z
user: login: heeeun-ko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T00:49:10Z
user: login: dark2138
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T00:52:14Z

Review comments:
diff_hunk: @@ -0,0 +1,233 @@
+package com.golden_dobakhe.HakPle.domain.post.post.service.impl;
+
+
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardRequest;
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardResponse;
+import com.golden_dobakhe.HakPle.domain.post.post.entity.*;
+import com.golden_dobakhe.HakPle.domain.post.post.exception.BoardException;
+import com.golden_dobakhe.HakPle.domain.post.post.repository.*;
+import com.golden_dobakhe.HakPle.domain.post.post.service.BoardService;
+
+import com.golden_dobakhe.HakPle.domain.user.entity.User;
+import com.golden_dobakhe.HakPle.domain.user.repository.UserRepository;
+import com.golden_dobakhe.HakPle.global.entity.Status;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class BoardServiceImpl implements BoardService {
+
+    private final BoardRepository boardRepository;
+    private final UserRepository userRepository;
+    private final HashtagRepository hashtagRepository;
+    private final TagMappingRepository tagMappingRepository;
+    private final BoardLikeRepository boardLikeRepository;
+    private final BoardReportRepository boardReportRepository;
+
+    @Override
+    @Transactional
+    public BoardResponse createBoard(BoardRequest request, Long userId) {
+        validateBoardRequest(request);
path: backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/impl/BoardServiceImpl.java
user: login: heeeun-ko
body: validateBoardRequest가 어디에서 나온건지를 모르겠습니다..! 제가 못찾고 있는거 같기도...😓
created_at: 2025-04-11T00:23:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+package com.golden_dobakhe.HakPle.domain.post.post.service.impl;
+
+
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardRequest;
+import com.golden_dobakhe.HakPle.domain.post.post.dto.BoardResponse;
+import com.golden_dobakhe.HakPle.domain.post.post.entity.*;
+import com.golden_dobakhe.HakPle.domain.post.post.exception.BoardException;
+import com.golden_dobakhe.HakPle.domain.post.post.repository.*;
+import com.golden_dobakhe.HakPle.domain.post.post.service.BoardService;
+
+import com.golden_dobakhe.HakPle.domain.user.entity.User;
+import com.golden_dobakhe.HakPle.domain.user.repository.UserRepository;
+import com.golden_dobakhe.HakPle.global.entity.Status;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class BoardServiceImpl implements BoardService {
+
+    private final BoardRepository boardRepository;
+    private final UserRepository userRepository;
+    private final HashtagRepository hashtagRepository;
+    private final TagMappingRepository tagMappingRepository;
+    private final BoardLikeRepository boardLikeRepository;
+    private final BoardReportRepository boardReportRepository;
+
+    @Override
+    @Transactional
+    public BoardResponse createBoard(BoardRequest request, Long userId) {
+        validateBoardRequest(request);
path: backend/src/main/java/com/golden_dobakhe/HakPle/domain/post/post/service/impl/BoardServiceImpl.java
user: login: dark2138
body: BoardServiceImpl 맨밑줄에 있습니다
created_at: 2025-04-11T00:52:14Z
author_association: COLLABORATOR

PR comments:
user: login: dark2138
created_at: 2025-04-10T13:47:11Z
body: 댓글 달아주세요
author_association: COLLABORATOR