SpringBoot 기반 여행 정보 공유 플랫폼 개발 (Java + JPA + Thymeleaf + MySQL)

이 시스템은 Spring Boot를 기반으로 구축된 여행 및 일상 정보 공유 웹 애플리케이션입니다. 사용자는 게시글, 사진첩, 댓글 등을 작성하고 관리할 수 있으며, 관리자 기능을 통해 콘텐츠를 효율적으로 운영할 수 있습니다.

기술 스택

  • 백엔드: Java 8+, Spring Boot, Spring MVC, Spring Data JPA
  • 프론트엔드: HTML5, Thymeleaf, JavaScript, jQuery, AJAX
  • 데이터베이스: MySQL 5.7 이상
  • 빌드 도구: Apache Maven
  • IDE: IntelliJ IDEA, Eclipse, STS 등 지원

핵심 기능

  • 사용자 인증 및 세션 기반 로그인
  • 여행 게시글 작성/수정/삭제 (썸네일 이미지 자동 생성)
  • 카테고리(타입)별 게시글 분류 및 조회
  • 사진첩(Atlas) 및 개별 사진(Picture) 관리
  • 댓글 및 대댓글(Reply) 기능
  • 검색 기능 (키워드 + 카테고리 조합)
  • 조회수 기반 인기 게시글 목록
  • 게시글 및 사진첩 페이징 처리
  • 관리자 전용 페이지 (블로그, 카테고리, 사진첩 관리)

주요 컨트롤러 예시

블로그 관리 컨트롤러

@RestController
public class BlogController {

    @Autowired
    private BlogService blogService;
    @Autowired
    private TypeService typeService;

    @PostMapping("/blogs")
    public ResponseEntity<String> createBlog(
            @RequestParam("tid") Integer typeId,
            @RequestParam("title") String title,
            @RequestParam("content") String content,
            @RequestParam("flag") String flag,
            @RequestParam(value = "image", required = false) MultipartFile image,
            HttpServletRequest request) throws IOException {

        User currentUser = (User) request.getSession().getAttribute("user");
        if (currentUser == null) {
            return ResponseEntity.status(401).body("Unauthorized");
        }

        Blog blog = new Blog();
        blog.setType(typeService.findById(typeId));
        blog.setTitle(title);
        blog.setContent(content);
        blog.setFlag(flag);
        blog.setPublish("published".equals(flag));
        blog.setUser(currentUser);
        blog.setCreateDate(new Date());
        blog.setUpdateDate(new Date());
        blog.setViews(0);
        blog.setNums(content.length());

        Blog savedBlog = blogService.save(blog);

        if (image != null && !image.isEmpty()) {
            String imagePath = saveBlogImage(savedBlog.getId(), image, request);
            blogService.updateFirstPicture(savedBlog.getId(), imagePath);
        }

        return ResponseEntity.ok("success");
    }

    private String saveBlogImage(Long blogId, MultipartFile image, HttpServletRequest req) throws IOException {
        String uploadDir = req.getServletContext().getRealPath("/img/picture");
        Path dirPath = Paths.get(uploadDir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }

        Path filePath = dirPath.resolve(blogId + ".jpg");
        image.transferTo(filePath.toFile());

        // JPG 변환 및 저장
        BufferedImage originalImg = ImageIO.read(filePath.toFile());
        BufferedImage jpgImg = new BufferedImage(originalImg.getWidth(), originalImg.getHeight(), BufferedImage.TYPE_INT_RGB);
        jpgImg.createGraphics().drawImage(originalImg, 0, 0, null);
        ImageIO.write(jpgImg, "jpg", filePath.toFile());

        return filePath.toString();
    }
}

댓글 및 대댓글 처리

@RestController
public class CommentController {

    @Autowired
    private CommentService commentService;
    @Autowired
    private ReplyService replyService;
    @Autowired
    private BlogService blogService;

    @PostMapping("/comments/{blogId}")
    public void addComment(@PathVariable Long blogId, HttpServletRequest request) {
        Blog blog = blogService.findById(blogId);
        Comment comment = new Comment();
        comment.setBlog(blog);
        comment.setContent(request.getParameter("content"));
        comment.setNickname(request.getParameter("nickname"));
        comment.setEmail(request.getParameter("email"));
        comment.setCreateTime(new Date());
        comment.setFlag(0);
        commentService.save(comment);
    }

    @GetMapping("/comments/{blogId}")
    public List<Comment> getCommentsWithReplies(@PathVariable Long blogId) {
        List<Comment> comments = commentService.findByBlogId(blogId);
        for (Comment c : comments) {
            List<Reply> replies = replyService.findByBlogAndComment(blogId, c.getId());
            c.setReplies(replies);
        }
        return comments;
    }
}

사진 관리 컨트롤러

@RestController
public class PictureController {

    @Autowired
    private PictureService pictureService;
    @Autowired
    private AtlasService atlasService;

    @PostMapping("/pictures")
    public ResponseEntity<String> uploadPicture(
            @RequestParam("aid") Long atlasId,
            @RequestParam("title") String title,
            @RequestParam("image") MultipartFile image,
            HttpSession session,
            HttpServletRequest request) {

        User user = (User) session.getAttribute("user");
        Atlas atlas = atlasService.findById(atlasId);

        Picture picture = new Picture();
        picture.setAtlas(atlas);
        picture.setUser(user);
        picture.setTitle(title);
        picture.setCreateDate(new Date());
        Picture savedPic = pictureService.save(picture);

        try {
            // 상세 이미지 저장
            String detailPath = request.getServletContext().getRealPath("/img/pictureDetail");
            saveImage(image, detailPath, savedPic.getId() + ".jpg");

            // 썸네일 생성 및 저장
            String thumbPath = request.getServletContext().getRealPath("/img/pictureThumbnail");
            generateThumbnail(detailPath, thumbPath, savedPic.getId() + ".jpg");
        } catch (IOException e) {
            return ResponseEntity.status(500).body("Image upload failed");
        }

        return ResponseEntity.ok("success");
    }

    private void saveImage(MultipartFile file, String dir, String filename) throws IOException {
        Path path = Paths.get(dir, filename);
        if (!Files.exists(path.getParent())) {
            Files.createDirectories(path.getParent());
        }
        file.transferTo(path.toFile());
    }

    private void generateThumbnail(String srcDir, String destDir, String filename) throws IOException {
        // 간단한 썸네일 생성 로직 (실제 구현은 ImageUtil 클래스 활용)
        BufferedImage original = ImageIO.read(Paths.get(srcDir, filename).toFile());
        int targetWidth = 200;
        int targetHeight = (int) ((double) original.getHeight() * targetWidth / original.getWidth());
        BufferedImage thumb = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = thumb.createGraphics();
        g.drawImage(original.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH), 0, 0, null);
        g.dispose();

        Path destPath = Paths.get(destDir, filename);
        if (!Files.exists(destPath.getParent())) {
            Files.createDirectories(destPath.getParent());
        }
        ImageIO.write(thumb, "jpg", destPath.toFile());
    }
}

프론트엔드 통합

Thymeleaf 템플릿 엔진을 사용하여 서버 사이드 렌더링을 수행하며, AJAX를 통해 비동기 요청을 처리합니다. 예를 들어, 댓글 작성 시 폼 데이터를 POST로 전송하고, 성공 시 화면을 새로 고치지 않고 DOM을 동적으로 갱신합니다.

보안 및 세션 관리

간단한 세션 기반 인증을 사용합니다. 로그인 시 사용자 객체를 HttpSession에 저장하고, 민감한 API에서는 세션 존재 여부를 확인합니다.

@GetMapping("/check-login")
public ResponseEntity<Map<String, Object>> checkLogin(HttpSession session) {
    Map<String, Object> response = new HashMap<>();
    if (session.getAttribute("user") != null) {
        response.put("status", "success");
    } else {
        response.put("status", "fail");
        response.put("message", "Not logged in");
    }
    return ResponseEntity.ok(response);
}

태그: SpringBoot jpa Thymeleaf MySQL JavaWeb

9월 25일 12:54에 게시됨