241128 뷰 (ajax, ajax-router)

241128 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.

 

AI 없이 코드를 짜는 현직자는 없다. AI를 잘 다룰줄 아는 것이 중요하다.
질문을 잘해야 대답을 잘해준다. 그렇기에 기초를 튼튼히 다지는 것이 중요하다.
또 툴을 잘 사용하여 개발 시간 최적화, 코드의 최적화를 잘하고 글로 잘 적어내어보자!

프로젝트 코드를 다 짜고 용어를 정확히 찝어가며 설명할 수 있어야 한다.
프로젝트를 통해서 최종적으로 온갖 것들을 공부하는 것이다.

뷰 실습 (ajax-axios 1)

axois cdn

<script src="https://unpkg.com/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> // 배포용
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.js"></script> // 개발용

JSON 데이터 : http://openapi.seoul.go.kr:8088/sample/json/SeoulLibraryTimeInfo/1/5/
JSON 구조 : SeoulLibraryTimeInfo 안에 row가 들어가있는 구조

ex15ajax_lib.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <div id="app">
        <button @click="fetchData">도서관 정보 읽기</button>
        <button @click="clearData">도서관 정보 삭제</button>

        <div v-for="lib in libraries" :key="lib.LBRRY_NAME">
            <h3>{{lib.LBRRY_NAME}}</h3>
            주소 : {{lib.ADRES}}<br/>
            전화 : {{lib.TEL_NO}}<hr/>
        </div>
    </div>

    <script>
        const {createApp} = Vue;
        
        createApp({
            data() {
                return {
                    libraries:[] // 도서관 정보를 담아둘 배열
                };
            },
            methods:{
                fetchData(){
                    axios.get("http://openapi.seoul.go.kr:8088/sample/json/SeoulLibraryTimeInfo/1/5/")
                    .then(response => {
                        this.libraries = response.data.SeoulLibraryTimeInfo.row;
                    })
                    .catch(error => {
                        console.error('fetch err : ', error);
                    })
                },
                clearData(){
                    this.libraries = []; // 배열 비우기
                },
            }
        }).mount("#app");
    </script>
</body>
</html>

읽기

삭제


뷰 실습 (ajax-axios 2)

위도, 경도 넣고 지역 날씨 정보 얻기

https://api.open-meteo.com/v1/forecast?latitude=38.146609&longitude=127.3132256&current_weather=true

ex16ajax_weather.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <div id="app">
        <button @click="fetchData">철원군 날씨 정보 얻기</button>

        <div v-if="weather">
            <h3>날씨 정보</h3>
            예보 시간 : {{weather.time}}<br/>
            온도 : {{weather.temperature}}<br/>
            풍속 : {{weather.windspeed}}<br/>
            풍향 : {{weather.winddirection}}<br/>
        </div>
    </div>

    <script>
        const {createApp} = Vue;
        
        createApp({
            data() {
                return {
                    weather:null
                };
            },
            methods:{
                fetchData(){
                    axios.get("https://api.open-meteo.com/v1/forecast?latitude=38.146609&longitude=127.3132256&current_weather=true")
                    .then(response => {
                        this.weather = response.data.current_weather;
                    })
                    .catch(error => {
                        console.error('fetch err : ', error);
                    })
                },
            }
        }).mount("#app");
    </script>
</body>
</html>

다른 도메인의 정보를 가져오는 것은 cors 정책에 위반되지만 해당 오픈api 서버들이 접근을 허용해주기 때문에 가능


뷰 실습 (ajax-axios 3)

우리가 서버를 만들고 데이터를 가져와보자! (이클립스, 아파치 톰캣 서버)

기존 실습했던 abcReact 자료 사용

test.jsp

<%@page import="org.json.simple.JSONObject"%>
<%@page import="org.json.simple.JSONArray"%>
<%@page import="java.sql.*"%>
<%@ page language="java" contentType="text/json; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
JSONArray jikwons = new JSONArray();

	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	String result = "";
	
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		String url = "jdbc:mariadb://localhost:3306/test";
		conn = DriverManager.getConnection(url, "root", "1111");
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	}
	try {
		pstmt = conn.prepareStatement("SELECT * FROM jikwon");
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			JSONObject obj = new JSONObject();
			obj.put("jikwonno", rs.getInt("jikwonno"));
			obj.put("jikwonname", rs.getString("jikwonname"));
			obj.put("jikwonjik", rs.getString("jikwonjik"));
			obj.put("jikwonpay", rs.getString("jikwonpay"));
			jikwons.add(obj);	
		}	
		out.print(jikwons.toString());
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	} finally {
		if (rs != null) rs.close();
		if (pstmt != null) pstmt.close();
		if (conn != null) conn.close();
	}
%>

서버 실행 후 JSON 객체의 직원 데이터 가져와보기 (서버 살려놓기)

http://localhost:8080/abcReact/test.jsp

ex17ajax_jikwon.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
    <button v-on:click="showFunc">직원 자료 보기</button>
    <table v-if="datas && datas.length > 0">
        <tr>
            <th>사번</th><th>이름</th><th>직급</th><th>연봉</th>
        </tr>
        <template v-for="data in datas">
        <tr>
            <td>{{data.jikwonno}}</td>
            <td>{{data.jikwonname}}</td>
            <td>{{data.jikwonjik}}</td>
            <td>{{data.jikwonpay}}</td>
        </tr>
        </template> 
    </table>
</div>

<script>
    const {createApp} = Vue;

    createApp({
        data() {
            return {
                datas:[]
            }
        },
        methods:{
            showFunc(){
                axios.get("http://localhost:8080/abcReact/test.jsp")
                .then(response => {
                    console.log(response.data);
                    this.datas = response.data;
                })
                .catch(error => {
                    console.log("에러 : ", error);
                })
            }
        }
    }).mount("#app");
</script>
</body>
</html>

cors 정책 위반으로 에러가 뜬다. 서버에서 다른 도메인의 접근을 허용해주어야한다.

다시 이클립스로 가서 접근을 허용해주자

cors 해결

test.jsp

// cors 해결
response.setHeader("Access-Control-Allow-Origin", "*"); // 헤더에 Access-Control-Allow-Origin 문구를 넣어준다.
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 요청 방식별 허용 여부

JSONArray jikwons = new JSONArray();

	... 생략
%>

cors 허용 후 데이터를 가져올 수 있다.


뷰 실습 (ajax-axios 4)

세 개의 테이블 조인 예제 사용

sprweb_webjpa_join3plus

3개의 테이블을 조인하고 데이터를 가져오는 것을 확인

Buser

package pack;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class Buser {
    @Id
    private int buserno;

    private String busername;
    private String buserloc;
    private String busertel;

    @OneToMany(mappedBy = "buser", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JsonIgnore
    private List<Jikwon> jikwons;
}

Jikwon

package pack;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Data
@NoArgsConstructor
public class Jikwon {
    @Id
    private int jikwonno;

    private String jikwonname;

    @ManyToOne
    @JoinColumn(name = "busernum")
    private Buser buser;

    private String jikwonjik;
    private int jikwonpay;
    private Date jikwonibsail;
    private String jikwongen;
    private String jikwonrating;

    @OneToMany(mappedBy = "jikwon", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JsonIgnore
    private List<Gogek> gogeks; // gogek와의 관계 추가
}

Gogek

package pack;

import com.fasterxml.jackson.annotation.JsonBackReference;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class Gogek {
    @Id
    private int gogekno;

    private String gogekname;
    private String gogektel;
    private String gogekjumin;

    @ManyToOne
    @JoinColumn(name = "gogekdamsano")
    @JsonBackReference // @ManyToOne에 대한 순환참조 방지
    private Jikwon jikwon; // 직렬화할때는 직원 데이터를 제외하고 진행한다.
}

JikwonDto

package pack;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class JikwonDto {
	private int jikwonno;
	private String jikwonname;
	private String buser; // 부서 정보를 가져오기 위함
	private String jikwonjik;
	private int jikwonpay;
	private String jikwonibsail;
	private String jikwongen;
	private String jikwonrating;
}

JoinController

package pack;

import java.util.List;

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

@RestController
public class JoinController {
    @Autowired
	private JoinService joinService;
    
//	private final JoinService joinService;
//
//    public JoinController(JoinService joinService) {
//        this.joinService = joinService;
//    }

    @GetMapping("/joindata")
    public List<BuserJikwonGogekDTO> getJoinData() {
        return joinService.getJoinedData();
    }
    
    @GetMapping("/busers")
    public List<Buser> getAllBusers() {
        return joinService.getAllBusers();
    }
    
    @GetMapping("/jikwons")
    public List<JikwonDto> getAllJikwons() {
        return joinService.getAllJikwons();
    }
    
    @GetMapping("/gogeks")
    public List<Gogek> getAllGogeks() {
        return joinService.getAllGogeks();
    }
}

JoinService

package pack;

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;

@Service
public class JoinService {
	
	private final JikwonRepository jikwonRepository;
	private final GogekRepository gogekRepository;

    private final BuserRepository buserRepository;

    public JoinService(BuserRepository buserRepository, JikwonRepository jikwonRepository, GogekRepository gogekRepository) {
        this.buserRepository = buserRepository;
        this.jikwonRepository = jikwonRepository;
        this.gogekRepository = gogekRepository;
        
    }

    public List<BuserJikwonGogekDTO> getJoinedData() {
        // DTO로 직접 데이터를 조회
        return buserRepository.findAllJoinedData();
    }
    
    public List<Buser> getAllBusers() {
        // Buser 데이터를 조회
        return buserRepository.findAll();
    }
    
    public List<JikwonDto> getAllJikwons() {
        // jikwon 데이터를 조회
        return jikwonRepository.findAll().stream()
        		.map(jikwon -> new JikwonDto(
        				jikwon.getJikwonno(),
        				jikwon.getJikwonname(),
        				jikwon.getBuser().getBusername(),
        				jikwon.getJikwonjik(),
        				jikwon.getJikwonpay(),
        				jikwon.getJikwonibsail().toString(),
        				jikwon.getJikwongen(),
        				jikwon.getJikwonrating()
        				)).collect(Collectors.toList());
    }
    
    public List<Gogek> getAllGogeks() {
        // gogek 데이터를 조회
        return gogekRepository.findAll();
    }
}
💡 fetch / await fetch 차이점
fetch :
/ 그냥 비동기로 실행
/ then 체인 사용하여 처리 (then이 길어지면 코드가 복잡해보임, 가독성이 떨어짐)
/ 마구 사용

await fetch :
/ 요청이 끝날 때까지 대기
/ try-catch로 처리(에러 처리가 쉽다)
/ async와 함께 사용

클라이언트단으로 이동

ex18ajax_route.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://unpkg.com/vue-router@4"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <style></style>
  </head>
  <body>
    <div id="app">
      <h1>자료보기</h1>
      <nav>
        <router-link to="/">전체자료</router-link> |
        <router-link to="/busers">부서자료</router-link> |
        <router-link to="/jikwons">직원자료</router-link> |
        <router-link to="/gogeks">고객자료</router-link>
      </nav>
      <router-view></router-view>
    </div>
    <script>
      // 전체자료 컴포넌트
      const AllData = {
        template: `
            <div>
                <h2>전체자료</h2>
                <table v-if="allData.length">
                    <thead>
                        <tr>
                            <th>부서번호</th><th>부서명</th><th>직원명</th><th>관리고객명</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="data in allData" :key="data.jikwonno">
                            <td>{{data.buserno}}</td>
                            <td>{{data.busername}}</td>
                            <td>{{data.jikwonname}}</td>
                            <td>{{data.gogekname}}</td>
                        </tr>    
                    </tbody>
                </table>
                <p v-else>데이터가 없어요!</p>
            </div>
            `,
        data() {
          return {
            allData: [],
          };
        },
        mounted() {
          axios
            .get("http://localhost/joindata")
            .then((response) => {
              this.allData = response.data;
            })
            .catch((error) => {
              console.log("err : ", error);
            });
        },
      };

      // 부서자료 컴포넌트
      const BuserData = {
        template: `
            <div>
                <h2>부서자료</h2>
                <table v-if="buserData.length">
                    <thead>
                        <tr>
                            <th>부서번호</th><th>부서명</th><th>위치</th><th>전화번호</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="data in buserData" :key="data.buserno">
                            <td>{{data.buserno}}</td>
                            <td>{{data.busername}}</td>
                            <td>{{data.buserloc}}</td>
                            <td>{{data.busertel}}</td>
                        </tr>    
                    </tbody>
                </table>
                <p v-else>데이터가 없어요!</p>
            </div>
            `,
        data() {
          return {
            buserData: [],
          };
        },
        mounted() {
          axios
            .get("http://localhost/busers")
            .then((response) => {
              this.buserData = response.data;
            })
            .catch((error) => {
              console.log("err : ", error);
            });
        },
      };

      // 직원자료 컴포넌트
      const JikwonData = {
        template: `
            <div>
                <h2>부서자료</h2>
                <table v-if="jikwonData.length">
                    <thead>
                        <tr>
                            <th>사번</th><th>이름</th><th>직급</th><th>연봉</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="data in jikwonData" :key="data.jikwonno">
                            <td>{{data.jikwonno}}</td>
                            <td>{{data.jikwonname}}</td>
                            <td>{{data.jikwonjik}}</td>
                            <td>{{data.jikwonpay}}</td>
                        </tr>    
                    </tbody>
                </table>
                <p v-else>데이터가 없어요!</p>
            </div>
            `,
        data() {
          return {
            jikwonData: [],
          };
        },
        mounted() {
          axios
            .get("http://localhost/jikwons")
            .then((response) => {
              this.jikwonData = response.data;
            })
            .catch((error) => {
              console.log("err : ", error);
            });
        },
      };

      // 고객자료 컴포넌트
      const GogekData = {
        template: `
            <div>
                <h2>고객자료</h2>
                <table v-if="gogekData.length">
                    <thead>
                        <tr>
                            <th>고객번호</th><th>고객이름</th><th>전화</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="data in gogekData" :key="data.gogekno">
                            <td>{{data.gogekno}}</td>
                            <td>{{data.gogekname}}</td>
                            <td>{{data.gogektel}}</td>
                        </tr>    
                    </tbody>
                </table>
                <p v-else>데이터가 없어요!</p>
            </div>
            `,
        data() {
          return {
            gogekData: [],
          };
        },
        mounted() {
          axios
            .get("http://localhost/gogeks")
            .then((response) => {
              this.gogekData = response.data;
            })
            .catch((error) => {
              console.log("err : ", error);
            });
        },
      };

      // Vue Router 설정
      const routes = [
        { path: "/", component: AllData },
        { path: "/busers", component: BuserData },
        { path: "/jikwons", component: JikwonData },
        { path: "/gogeks", component: GogekData },
      ];

      // 라우터 생성
      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes,
      });

      // Vue 앱 생성
      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

에러

🚨 CORS 
1. Controller에 @CrossOrigin(origins = "도메인")을 걸어준다.
2. WebConfig 클래스를 만들어 접근을 허용해준다.

WebConfig

package pack;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    	registry.addMapping("/**")
        .allowedOriginPatterns("*") // “*“같은 와일드카드를 사용
        .allowedMethods("GET", "POST") // 허용할 HTTP method
        .allowCredentials(true); // 쿠키 인증 요청 허용
    }
}

결과

 

 

뷰로 작성한 html을 static에 넣고 실행시켜도 잘 작동한다.