241129 뷰(RESTful)

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

 

코드를 짤 때 시큐어 코딩, UX / UI를 최대한 고려해서 코드를 짤려고 노력해야한다!

이전 글과 이어서 진행됩니다.

html 파일 내에 코드가 너무 길어 각 컴포넌트, 라우터 분리하기! 이클립스로 돌아가자!

static에 components, router 폴더 생성

myrouter.js

(static/router)

import {createApp} from 'vue';
import {createRouter, createWebHistory} from 'vue-router';
import AllData from "../components/alldata.js";
import BuserData from "../components/buserdata.js";
import JikwonData from "../components/jikwondata.js";
import GogekData from "../components/gogekdata.js";

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

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

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

 

alldata.js

export default {
	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);
	            });
	        },
}

buserdata.js

export default {
	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);
	            });
	        },
}

jikwondata.js

export default {
	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);
	            });
	        },
}

gogekdata.js

export default {
	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);
	            });
	        },
}

ajaxroute2.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>
    <script type="module" src="./router/myrouter.js" defer></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>
  </body>
</html>

에러

🚨 ajaxroute2.html:1 Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".

Vue.js와 Vue Router를 CDN에서 직접 가져오는 경우:
type="module" 속성을 사용하여 모듈 방식으로 JavaScript 파일을 로드할 때는, 모듈을 CDN에서 가져오는 것이 아닌, 직접 경로를 지정해야 합니다. CDN에서 가져온 Vue.js와 Vue Router는 전역적으로 사용할 수 있지만, 모듈 방식에서는 직접 가져와야 합니다.

Vue.js와 Vue Router를 전역으로 사용하기:
myrouter.js 파일에서 Vue.js와 Vue Router를 전역으로 사용하도록 코드를 수정할 수 있습니다. 예를 들어, createApp과 createRouter를 사용할 때 전역으로 설정한 Vue를 참조하게 됩니다.

모듈을 사용할 때의 올바른 경로 지정:
myrouter.js 파일 내에서 Vue.js 및 Vue Router를 가져오는 방법을 수정합니다.

import {createApp} from 'vue';
import {createRouter, createWebHistory} from 'vue-router';
                                  🔽
// myrouter.js 수정
const { createApp } = Vue;
const { createRouter, createWebHistory } = VueRouter;

🚨 [Vue Router warn]: No match found for location with path "/ajaxroute2.html"

작동에는 문제가 없지만 Vue Router가 ajaxroute2.html 경로를 찾지 못했다 경고가 뜬다.

잘못된 URL 접근:
실제로 애플리케이션을 실행할 때 브라우저의 주소창에 /ajaxroute2.html을 입력하여 Vue Router가 해당 경로에 대한 정의가 없기 때문에 경고가 발생했다.

정확한 경로 사용:
Vue Router를 사용했을 때 애플리케이션 내에서 적절한 링크를 사용하여 경로를 탐색해야 하겠다. Vue Router를 통해 정의된 경로만 사용해야 하며, 직접 URL을 입력하는 것은 피하는 것이 좋을 것 같다.

myrouter.js (수정)

// import {createApp} from 'vue';
// import {createRouter, createWebHistory} from 'vue-router';
import AllData from "../components/alldata.js";
import BuserData from "../components/buserdata.js";
import JikwonData from "../components/jikwondata.js";
import GogekData from "../components/gogekdata.js";
import NotFound from '../components/notfound.js';

// Vue Router 설정
const { createApp } = Vue;
const { createRouter, createWebHistory } = VueRouter;
const routes = [
	{ path: "/", name: "all", component: AllData },
	{ path: "/busers", name: "busers", component: BuserData },
	{ path: "/jikwons", name: "jikwons", component: JikwonData },
	{ path: "/gogeks", name: "gogeks", component: GogekData },
	{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },  // 해결책
];

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

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

notfound.js

export default {
    template: `<h3>Hi~!</h3>`,
}

결과


뷰 실습 (RESTful : CRUD)

product 테이블 생성

create table product(
code varchar(50) primary key,
name varchar(255) not null,
description text,
price int not null,
image varchar(255) default 'https://t1.daumcdn.net/daumtop_deco/images/pctop/2023/logo_daum.png');

insert into product (code,name,description,price) values ('p1','볼펜','부드럽게 글씨가 써진다.',3000);
insert into product (code,name,description,price) values ('p2','연필','사각사각 글씨가 써진다.',1500);

select * from product;

의존성 추가

application.properties

spring.application.name=sprweb50restful_vue

server.port=80
spring.thymeleaf.cache=false

#mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=1111

#mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=pack.dto.**

ProductMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="product">
	<select id="getAll" resultType="productDto">
		select * from product
	</select>
	<select id="getData" parameterType="String" resultType="productDto">
		select * from product where code=#{code}
	</select>

	<insert id="insert" parameterType="productDto">
		insert into product (code,name,description,price) values(#{code},#{name},#{description},#{price})
	</insert>
	<update id="update" parameterType="productDto">
		update product set name=#{name},description=#{description},price=#{price} where code=#{code}
	</update>
	<delete id="delete" parameterType="String">
		delete from product where code=#{code}
	</delete>
</mapper>

build.gradle

(swagger 의존성 추가)

dependencies {
         ...
         implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
}

ProductProcess

package pack.repository;

import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import pack.dto.ProductDto;

@Repository
public class ProductProcess {
	@Autowired
	private SqlSession session;
	
	public List<ProductDto> getAll() {
		return session.selectList("product.getAll"); // mapper의 namespace.id
	}
	
	public ProductDto getData(String code) {
		return session.selectOne("product.getData", code);
	}
	
	public void insert(ProductDto dto) {
		session.insert("product.insert", dto);
	}
	
	public void update(ProductDto dto) {
		session.update("product.update", dto);
	}
	
	public void delete(String code) {
		session.delete("product.delete", code);
	}
}

ProductController

package pack.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import pack.dto.ProductDto;
import pack.repository.ProductProcess;

@RestController
@RequestMapping("/products")
public class ProductController {
	@Autowired
	private ProductProcess productProcess;
	
	@GetMapping
	public ResponseEntity<List<ProductDto>> getAll() {
		List<ProductDto> products = productProcess.getAll();
		return ResponseEntity.ok(products); // HTTP 응답을 받환 200
	}
	
	@PostMapping
	public ResponseEntity<String> insert(@RequestBody ProductDto dto) {
		productProcess.insert(dto);
		return ResponseEntity.ok("상품 추가 성공");
	}
	
	@GetMapping("/{code}")
	public ResponseEntity<ProductDto> getData(@PathVariable("code")String code) {
		ProductDto product = productProcess.getData(code);
		if(product != null) {
			return ResponseEntity.ok(product); // HTTP 응답 200을 받환 
		} else {
			return ResponseEntity.notFound().build(); // HTTP 응답 404을 받환 
		}
	}
	
	@PutMapping("/{code}")
	public ResponseEntity<String> update(@PathVariable("code")String code, 
									@RequestBody ProductDto dto) {
		// 수정 대상 상품 존재 여부 확인
		ProductDto product = productProcess.getData(code);
		if(product == null) {
			return ResponseEntity.notFound().build(); // HTTP 응답 404을 받환
		}
		dto.setCode(code);
		productProcess.update(dto);
		return ResponseEntity.ok("상품 수정 성공");
	}
	
	@DeleteMapping("/{code}")
	public ResponseEntity<String> delete(@PathVariable("code")String code) {
		// 삭제 대상 상품 존재 여부 확인
		ProductDto product = productProcess.getData(code);
		if(product == null) {
			return ResponseEntity.notFound().build(); // HTTP 응답 404을 받환
		}
		productProcess.delete(code);
		return ResponseEntity.ok("상품 삭제 성공");
	}
}

각 요청 작동 확인

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</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">
	<nav>
		<router-link to="/">자료보기</router-link> |
        <router-link to="/add">자료추가</router-link> |
	</nav>
	<router-view></router-view>
</div>

<script type="module">
import ViewProduct from "../components/viewproduct.js";
// import AddProduct from "../components/addproduct.js";
// import EditProduct from "../components/editproduct.js";

const routes = [
	{ path: "/", component: ViewProduct },
	//{ path: "/add", component: AddProduct },
	//{ path: "/edit/:code", component: EditProduct },
];

const router = VueRouter.createRouter({
	history: VueRouter.createWebHashHistory(),
	routes,
});

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

viewproduct.js

export default {
	template:`
		<div>
			<h2>제품 정보</h2>
			<table>
				<tr>
					<th>코드</th><th>이미지</th><th>제품명</th><th>가격</th><th>설명</th><th>작업</th>
				</tr>
				<tr v-for="product in products" :key="product.code">
					<td>{{product.code}}</td>
					<td>
						<img :src="product.image" alt="제품이미지" />
					</td>
					<td>{{product.name}}</td>
					<td>{{product.price}}</td>
					<td>{{product.description}}</td>
					<td>
						<button @click="editProduct(product.code)">수정</button> /
						<button @click="deleteProduct(product.code)">삭제</button>
					</td>				
				</tr>
			</table>
		</div>
	`,
	data(){
		return {
			products:[]
		};
	},
	methods:{
		fetchProducts() {
			axios.get("/products")
			.then(response => {
				this.products = response.data;
			})
			.catch(error => {
				console.error("읽기 오류", error);
			});
		}
	},
	created(){ // 컴포넌트가 인스턴스된 직후(랜더링 전)에 호출
		this.fetchProducts()
	}
}

결과