중앙정보기술인재개발원/Spring

[Spring] final project_3차

soidev 2025. 5. 12. 17:55

REST API (백엔드)

  • REST API는 백엔드에서 데이터를 처리하고 응답을 JSON으로 주는 방식.
  • @RestController는 반환값이 html이 아니라 json이라는 걸 의미함.
  • 사용자는 서버에 데이터를 요청하면, 서버는 JSON으로 응답을 돌려줌.

→ 예) /api/user/isExistsAccountName?accountName=abc 요청하면

{ "result": true } 같은 JSON 응답이 옴.

AJAX (프론트엔드)

  • AJAX는 HTML 페이지를 새로고침하지 않고, 자바스크립트를 통해 서버에 비동기 요청을 보내는 기술.
  • 과거엔 XMLHttpRequest를 썼지만, 지금은 fetch()로 많이 씀.

→ 즉, 사용자가 아이디를 입력하면 fetch()로 서버에 REST API 요청 → JSON 응답 받기

→ 페이지 새로고침 없이 바로 결과 보여줌!

 

 

📘 추가 개념 정리: 웹브라우저 역할 & @ResponseBody / @RestController


💡 왜 아이디 중복 확인은 백엔드에서 해야 할까?

  • 웹브라우저(프론트)는 HTML, CSS, JavaScript만 다룸 → DB 접근 못 함
  • 아이디가 이미 있는지 확인하려면 → DB에서 SELECT COUNT(*) 같은 쿼리 필요
  • 그래서 **백엔드 서버(Spring)**가 DB를 조회하고 결과를 알려줘야 함

📌 즉, 백엔드에 요청(Request)을 보내서 결과(Response)를 받아오는 구조 필요


 @ResponseBody란?

  • 이 어노테이션을 쓰면, **함수의 return 값이 HTML이 아니라 데이터(JSON)**로 클라이언트에 응답됨
  • 보통 컨트롤러 메서드는 View 이름(html 파일 경로)을 리턴하는데,
  • @ResponseBody가 붙으면 → return 값이 JSON, 문자열, 숫자 그대로 전송됨

 @RestController란?

  • @Controller + @ResponseBody를 합친 것
  • 이 어노테이션을 클래스에 붙이면 → 이 안의 모든 메서드는 자동으로 JSON 리턴
  • 매번 @ResponseBody 쓰기 귀찮을 때 클래스 단위로 처리 가능

 

👉 그래서 JSON API 만들 땐 대부분 @RestController를 씀

  • 근데 리턴타입을 Map<String, Object>으로 많이 사용함
  • 그 이유 : json은 key-value 구조다 → Java의 Map<String, Object>와 매우 유사함
  • 이걸 Java에서 간단하게 표현하려면 Map이 가장 편하다:

그럼 이걸로 뭘 해야 돼?

  • 사용자 입력값(아이디)을 백엔드 함수의 파라미터로 전달
  • 백엔드는 이 값을 기반으로 DB에서 중복 여부를 확인
  • 확인한 값을 JSON 형식으로 리턴
  • 프론트는 JSON 응답을 받아서 JS로 사용자한테 메시지 보여줌

 

회원가입 아이디 중복 체크

register.html

<form id="registerForm" action="/user/registerProcess" method="post">
<!-- 한글이 들어갈 수 있음 -->
아이디 : <input onblur="confirmAccountName()" id="inputAccountName" name="accountName" type="text">
<input onclick="confirmAccountName()" type="button" value="아이디 중복확인">
<br>
<div id="accountnameErrorTextBox"></div>

button추가 중복확인!

 

RestController.java

package com.ca.finalproject.user.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.ca.finalproject.user.service.UserServiceImpl;

@RestController //@Controller + @ResponseBody : body는 json이다
@RequestMapping("api/user")
public class RestUserController {

@Autowired
private UserServiceImpl userService;

@RequestMapping("isExistsAccountName")
public Map<String,Object> isExistsAccountName(@RequestParam("accountName") String accountName){
Map<String,Object> map = new HashMap<>();
map.put("result", userService.isExistsAccountName(accountName));
return map;
}
}
  • @RestController: 반환값을 JSON 형태로 자동 변환
  • @RequestParam("accountName"): 쿼리스트링에서 파라미터 받음
  • Map<String,Object> { "result": true/false } 형태 반환


UserSqlMapper.java

//회원 존재 여부
public int countuserByAccountName(String accountName);

 

UserSqlMapper.xml

<select id="countuserByAccountName">
select count(*) from fp_user fu where fu.account_name=#{accountName}
</select>

 

register.html

function confirmAccountName(){
//아이디를 눌렀을때에 그 아이디가 필요한것?
const inputAccountName = document.getElementById("inputAccountName");
// inputAccountName.value; // 이 값ㅂ이 이미 가입한 녀석이냐?
 
//restapi를 이제 js로 호출할 것
//AJAX - js로 링크 변경 없이 페이지 내에서 백엔드에 요청(requeset) 하는 기술
fetch(`/api/user/isExistsAccountName?accountName=${inputAccountName.value}`)
.then(response=>response.json())
.then(json =>{ //여기가 reutnr된 값임
//여기서부터는 그냥 js 일반적인 능력 활용 필요!! 나중엔 여기가 죽어나갈 구간
// 백엔드 응답 후 여기부터 실행됨!!(비동기)
const accountnameErrorTextBox = document.getElementById("accountnameErrorTextBox")
if(json.result==true){
accountnameErrorTextBox.style.color="red";
accountnameErrorTextBox.innerText="이미 사용중인 아이디입니다."
isConfirmedAccountname=false;
}else{
accountnameErrorTextBox.style.color="green";
accountnameErrorTextBox.innerText="사용 가능한 아이디입니다."
isConfirmedAccountname=true;
}
})
;
}
  • fetch()로 서버에 AJAX 방식의 요청을 보냄 (accountName=입력값)
  • .then(response => response.json()): JSON 파싱
  • .then(json => {...}): 결과에 따라 텍스트 색과 메시지 변경

좋아요기능 구현

-- 좋아요T

CREATE TABLE fp_like (

id INT PRIMARY KEY AUTO_INCREMENT ,

article_id INT,

user_id INT,

created_at DATETIME DEFAULT NOW()

);

 

LikeDto.java

 
package com.ca.finalproject.dto;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class LikeDto {
    private int id;
    private int articleId;
    private int userId;
    private LocalDateTime createdAt;
}

BoardSqlMapper.java

 
 //좋이요
    public void createLike(@Param("articleId") int articleId,@Param("userId") int userId);
    public void deleteLike(@Param("articleId") int articleId,@Param("userId") int userId);
    public int countByArticleIdAndUserId(@Param("articleId") int articleId,@Param("userId") int userId);
    public int countByArticleId(@Param("articleId") int articleId);

BoardSqlMapper.xml

<!-- 좋아요 -->
<insert id="createLike">
	Insert into fp_like(article_id,user_id) VALUES(#{articleId},#{userId});
</insert>

<delete id="deleteLike">
	delete from fp_like where article_id=#{articleId}, and user_id=#{userId};
</delete>

<select id="countByArticleIdAndUserId">
	select count(*) from fp_like fl where fl.article_id=#{articleId} and fl.user_id=#{userId};
</select>

<select id="countByArticleId">
	select count(*) from fp_like fl where fl.article_id=#{articleId}
</select>

BoardServiceImpl.java

 
 //좋아요
    public void like(int articleId, int userId){
        boardSqlMapper.createLike(articleId, userId);
    }

    public void unlike(int articleId, int userId){
        boardSqlMapper.deleteLike(articleId, userId);
    }

    public boolean isMyLike(int articleId,int userId){
        return boardSqlMapper.countByArticleIdAndUserId(articleId, userId)>0;
    }

    public int getTotalLikeCount(int articleId){
        return boardSqlMapper.countByArticleId(articleId);
    }

RestBoardController.java

 
package com.ca.finalproject.board.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.ca.finalproject.board.service.BoardServiceImpl;
import com.ca.finalproject.dto.UserDto;

import jakarta.servlet.http.HttpSession;

@RestController
@RequestMapping("api/board")
public class RestBoardController {

    @Autowired
    private BoardServiceImpl boardService;

    @RequestMapping("like")
    public Map<String,Object> like(HttpSession session, @RequestParam("articleId") int articleId){
        UserDto sessionUserInfo = (UserDto)session.getAttribute("sessionUserInfo");
        boardService.like(articleId, sessionUserInfo.getId());

        Map<String,Object> map = new HashMap<>();
        map.put("result", "success");
        return map;
    }

    @RequestMapping("unlike")
    public Map<String,Object> unlike(HttpSession session, @RequestParam("articleId") int articleId){
        UserDto sessionUserInfo = (UserDto)session.getAttribute("sessionUserInfo");
        boardService.unlike(articleId, sessionUserInfo.getId());

        Map<String,Object> map = new HashMap<>();
        map.put("result", "success");
        return map;
    }

    @RequestMapping("isLiked")
    public Map<String, Object> isLiked(HttpSession session, @RequestParam("articleId") int articleId){
        Map<String, Object> map = new HashMap<>();
        UserDto sessionUserInfo = (UserDto)session.getAttribute("sessionUserInfo");
        map.put("result", boardService.isMyLike(articleId, sessionUserInfo.getId()));
        return map;
    }

    @RequestMapping("totalLikeCount")
    public Map<String,Object> totalLikeCount(@RequestParam("articleId") int articleId){     //이경우는 세션 필요없음 로그인 하지 않아도 카운트가 보여야하니까..
        Map<String, Object> map = new HashMap<>();

        map.put("result", boardService.getTotalLikeCount(articleId));

        return map;
    }
}

readArticle.html

  • <script>안에다가 넣기
function like(){
    //로그인 안 했을때
    if(mySessionId==null){ //null인 경우는 로그인 하지않았따
    //alert과 비슷한데 버튼이 두개 나옴
    if(confirm("로그인 후 이용 가능합니다. 로그인 페이지로 넘어갈래?")){
    	location.href="/user/loginPage";
    }
    return;
	}

    //로그인 했을 때 : like insert하면됨
    fetch(`/api/board/like?articleId=${articleId}`)
        .then(response=> response.json())
        .then(json=>{
            renderHeart();
            renderTotalCount();
    	});
    }

function unlike(){
    //로그인 했을 때 : like insert하면됨
    fetch(`/api/board/unlike?articleId=${articleId}`)
        .then(response=> response.json())
        .then(json=>{
            renderHeart();
            renderTotalCount();
    });
}

function renderTotalCount(){
    fetch(`/api/board/totalLikeCount?articleId=${articleId}`)
        .then(response=>response.json())
        .then(json=>{
            const countBox= document.getElementById("countBox");
            countBox.innerText=json.result;
    });
}

function renderHeart(){
            fetch(`/api/board/isLiked?articleId=${articleId}`)
                .then(response=>response.json())
                .then(json=>{
                    const heartBox = document.getElementById("heartBox");
                    if(json.result == true){
                        heartBox.classList.add("bi-heart-fill");
                        heartBox.classList.remove("bi-heart");
                        heartBox.setAttribute("onclick","unlike()");
                    }else{
                        heartBox.classList.remove("bi-heart-fill") ;
                        heartBox.classList.add("bi-heart");
                        heartBox.setAttribute("onclick","like()");
                    }
                });
 }
        

window.addEventListener("DOMContentLoaded",()=>{
            //여기가 사실상 시작점
            setMySessionId();
            renderTotalCount();
});

 

  • body에다가 넣기
<!-- 추천 -->
<div>
    <i id="heartBox" onclick="like()" class="bi bi-heart text-danger"></i> <span id="countBox"></span>
</div>