로컬
main(pc안에서 동작) → exe 파일 만들 수 있음
웹서비스
전송방식 → GET, POST
request(요청), response(응답) 객체 필요 → 톰캣이 주입해 줌(servlet-api.jar, jsp-api.jar)
URL → 요청 → 사용자의 요구사항을 구분
프로토콜://도메인주소:포트번호/폴더이름(업무명)/요청이름?(쿼리스트링)키와밸류&(&으로구분)키와밸류
http/https 프로토콜(stateless 비상태, 상태유지 x) → 클라이언트 요청 오면 연결, 전달 후 바로 요청 끊어버림
인터프리터 역할(웹서비스)
브라우저 / NodeJS → JS에 import(babel에서 사용), require(nodeJS에서 사용) 등장
servlet
HttpServlet상속받아야 서블릿 → @Override 가능 doGet(요청, 응답), doPost(req, res)
서블릿은 view역할 담당하기엔 힘들다(태그를 out.print(태그) 이렇게 하나하나 써야 하기에)
Printwriter out = res.getWrite(); → new가 아니라 메소드로 인스턴스화 → if문 사용, 조건에 따라 객체생성 가능
out.print(태그); → 브라우저에 써줌, 이렇게 하면 그냥 문자열(마임타입 html 지정해줘야 함)
res.setContentType(”text/html”) → 이렇게 마임타입 지정해 주면 확장자가 java여도 html로 인식됨
서블릿은 싱글톤으로 관리됨(WAS가 관리) → web.xml 혹은 @WebServlet 해야 함(req, res를 관리받는 것)
객체가 하나로만 관리됨 → 스레드 필요(순서대로 처리, 경합이 벌어짐)
if( x == null) {
x = new Sonata();
}
spring에선 이런 req, res가 없어도 요청, 응답 가능 → req, res에 의존적이지 않다
java > servlet > jsp
jsp
표준서블릿, 요청을 jsp가 받으면 mvc패턴 아님!
(인스턴스화를 할 수 없음(WAS마다 이름이 다르기에 a.jsp → a_jsp.java → a_jsp.class)
servlet으로 view계층 표현하기 불편하기에 jsp 탄생
자바코드 최소한으로 작성, jsp라고 쓰고 html이라 읽는다(JS, HTML, CSS)
servlet
자바니까 상속가능(확장) / 사용자정의 서블릿일수도
response - 마임타입을 정한다. out객체를 만든다, sendRedirect 할 수 있다
request - 요청할 수 있다, 저장할 수 있다(프론트와 백 사이에-http프로토콜이라 상태가 끊어지니 상태유지, 기억하기 위해 저장)
<pojo step 1 - Pattern.java 패턴 테스트>
package com.pojo.step1;
public class Pattern {
public static void main(String[] args) {
// String url = "/chat_banana/업무이름/페이지이름|요청이름";
String url = "/chat_banana/dept/getDeptList.kh";
String context = "chat_banana/";
// 톰캣서버에 요청할 때 사용되는 주소값을 가지고 업무명과 업무에 필요한 이름으로 분리시켜
// 사용자 요청에따라 처리를 담당할 XXXController 객체를 주입하는데 사용함
String command = url.substring(context.length()+1);
System.out.println(command); // dept/getDeptList.kh
int end = command.lastIndexOf(".");
System.out.println(end);
// 16
command = command.substring(0, end);
System.out.println(command);
// dept/getDeptList
String upmu[] = null;
upmu = command.split("/");
for(String imsi : upmu) {
System.out.println(imsi);
// dept
// getDeptList
}
}
}
<pojo step 1 - web.xml FrontController1 추가>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" version="4.0">
<!--
web.xml(배치서술자 Deployment Descriptor)은 톰캣에서 제공해준다 -> 위치는 WEB-INF 아래
서버가 처음 로딩 시 web.xml 파일을 읽어들여 환경설정에 대해 어플리케이션을 배치하게 됨
-->
<!-- context-param에 등록한 정보는 톰캣서버가 기동할 때 생성되서 서버가 죽을때까지 쭉 기억한다 -->
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/classes/log4.properties</param-value>
</context-param>
<!--
자바로 웹서비스X / 서블릿은 가능 -> URL을 적을 수 있어서 -> xml에 등록가능(URL사용위해 서블릿을 배치)
서블릿과 서블릿매핑은 항상 같이있어야한다
URL을 등록(url-pattern) - URL이 있어야 요청할 수 있음
servlet보다 servlet-mapping을 먼저 읽는다
-->
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.mvc.controller.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test/test.do</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.day1.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/day1/hello.kh</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>DeptServlet</servlet-name>
<servlet-class>com.day1.DeptManager</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DeptServlet</servlet-name>
<url-pattern>/jEasyUI/dept/insertAction.do</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>FrontController1</servlet-name>
<servlet-class>com.pojo.step1.FrontMVC1</servlet-class>
</servlet>
<!--
*.st1하면 어떤 요청이름이 오든 확장자가 st1으로 끝나면 FrontMVC1 서블릿이 인터셉트함
web.xml을 수정하면 무조건 서버를 재기동해야 수정된 내용이 반영됨
-->
<servlet-mapping>
<servlet-name>FrontController1</servlet-name>
<url-pattern>*.st1</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
</web-app>
<!--
Deployment Descriptor 배치서술자 spring maven방식 빌드 -> pom.xml파일에 등록
web.xml문서를 수정하면 반드시 서버를 내렸다가 다시 기동할 것(주의!)
-->
<pojo step 1 - Action.java>
package com.pojo.step1;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
클래스 설계에 인터페이스가 필요하다
인터페이스 중심의 코딩을 전개하는 것이 결합도를 낮춰준다, 의존성이 낮다, 단위테스트 가능 - 신뢰도 높이는 코드
HttpServlet에서 강제(@Override: doGet, doPost)하는 void를 다른 타입으로 바꾸어 보자
그래서 아래와같이 바꾸었지만 파라미터 자리의 req와 res는 개발자가 인스턴스화하는 것이 아니고
톰캣이 주입해주는 객체이다.
이 문제를 어떻게 해결하는지가 관전포인트!
*/
public interface Action {
public ActionForward execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
}
<pojo step 1 - ActionForward.java>
package com.pojo.step1;
public class ActionForward {
/*
페이지 이동 방법
1. res.sendRedirect("XXX.jsp") -표준서블릿(jsp)가 요청 받아줌 -> MVC패턴이 아니다!
or res.sendRedirect("dept/getDeptList.kh") - 서블릿이 요청 받아줌 -> MVC패턴인가?
jsp가 요청을 받는 것이 왜 문제인가?
WAS마다 명명규칙이 다르다. 그래서 인스턴스화를 할 수 없다
또한 인스턴스화를 한다 하더라도 request와 response객체를 주입받지 못하는 장애가 있다
(요청, 응답, 마임타입 설정 등등 많은 것들을 못한다!)
*/
private String path = null; // 응답페이지 이름 또는 서블릿의 이름
// sendredirect로 페이지를 이동할 것인지
// forward로 페이지를 이동할 것인지 여부를 결정함
private boolean isRedirect = false; // true: redirect(insert|update|delete), false: forward(select)
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isRedirect() {
return isRedirect;
}
public void setRedirect(boolean isRedirect) {
this.isRedirect = isRedirect;
}
}
/*
ActionForward 클래스를 왜 만드는가?
웹서비스에서는 main 메소드 대신에 URL로 요청을 보낸다
누가? 클라이언트(사용자)가, 누구에게? Tomcat서버에게 요청한다
요청을할 때 URL을 사용한다
*/
글목록 보기
1. 기본 10건은 항상 최신글로 조회결과를 보여준다 → 서블릿 - jsp (페이지를 출력하기 전에 DB에서 최신 10건의 글을 가져와야 함)
2. 무조건 조건검색을 통해서 조회결과를 본다 → jsp - 서블릿 - jsp
새 글쓰기
jsp - 서블릿(insert) - 서블릿(select) → jsp (작성한 글을 db에 저장하고 목록엔 새 글까지 출력해야 하기에 다시 db에서 가져와야 함)
댓글 쓰기
read.jsp(상세 보기, 기본정보 가지고 있음)
서블릿(select 1건) - jsp - 댓글쓰기버튼 - jsp - 서블릿(insert) - 서블릿(select) - jsp
글 수정
비밀번호인증 후에 수정할 수 있음
수정버튼 누르면 jsp - 비번입력 - 서블릿(update) - 서블릿(select) - jsp
글 삭제
비밀번호인증 후에 삭제할 수 있음
삭제버튼 누르면 jsp - 비번입력 - 서블릿(delete) - 서블릿(select) - jsp
<pojo step 1 - FrontMVC1.java>
package com.pojo.step1;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
public class FrontMVC1 extends HttpServlet {
Logger logger = Logger.getLogger(FrontMVC1.class);
protected void doService(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
logger.info("doService 호출");
String uri = req.getRequestURI();
logger.info(uri); // /dept/getDeptList.st1이 찍힘
String context = req.getContextPath(); // "/" -> server.xml에 들어있음
logger.info(context);
// 테스트: http://localhost:9000/업무폴더명/요청기능이름.st1
// 예시1: http://localhost:9000/dept/getDeptList.st1
// 예시2: http://localhost:9000/member/getMemberList.st1
// 예시3: http://localhost:9000/board/getBoardList.st1
// 예시4: http://localhost:9000/board/BoardInsert.st1
// 예시5: http://localhost:9000/board/BoardUpdate.st1
// 예시6: http://localhost:9000/board/BoardDelete.st1
String command = uri.substring(context.length() + 1);
System.out.println(command); // dept/getDeptList.st1
int end = command.lastIndexOf("."); // end는 .st1 잘라내기위해 사용
System.out.println(end); // 16(dept의 경우)
command = command.substring(0, end);
System.out.println(command); // dept/getDeptList
String upmu[] = null; // upmu[0]=업무명(폴더명), upmu[1]=요청기능이름(메소드명)
upmu = command.split("/");
for (String imsi : upmu) {
System.out.println(imsi); // upmu[0]=dept, upmu[1]=getDeptList
}
// 게으른 인스턴스화 - 필요할 때 주입
// 아직 업무명이 결정되지 않았다 - 업무명(upmu[0])이 Controller클래스의 접두어이다
DeptController deptController = null;
EmpController empController = null;
ActionForward af = null;
/*
* DeptController에서 req와 res를 받을 수 있는건 execute(req, res)메소드 뿐이다 그렇기에 각 기능별로
* 사용하려면 메소드로 나누면 안될 것 같다 그럼 if문으로 분기하고 DeptLogic.java에서 메소드를 분리해보자
*/
// request 객체는 저장소이다 - setAttribute, getAttribute
// req.setAttribute로 upmu[]를 DeptController로 전달(메소드에서 사용하기위해서)
req.setAttribute("upmu", upmu);
if ("dept".equals(upmu[0])) {
// 인스턴스화 -> execute() 호출하기위해 -> 안하면 NullPointerException이니까(서버쪽은 500번)
deptController = new DeptController();
// deptController는 서블릿이 아니여서 req, res를 WAS로부터 주입받을 수 없다(상속안받았기에)
// 그렇기에 af execute() 호출
af = deptController.execute(req, res);
} else if ("emp".equals(upmu[0])) {
// 인스턴스화
empController = new EmpController();
af = empController.execute(req, res);
}
// 페이지 이동 처리 공통코드로 만들기
// res.sendRedirect("/dept/getDeptList.jsp"); -> jsp(요청) - 서블릿 - jsp(응답)
// res.sendRedirect("/dept/getDeptList.st1"); -> jsp - 서블릿 - 서블릿 - jsp
if (af != null) {
if (af.isRedirect()) {
res.sendRedirect(af.getPath());
} else {
RequestDispatcher view = req.getRequestDispatcher(af.getPath());
view.forward(req, res);
}
} // end of 페이지 이동처리에 대한 공통 코드 부분
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
doService(req, res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
doService(req, res);
}
}
DeptController에서 req와 res를 받을 수 있는 건 execute(req, res)메소드 뿐이다
그렇기에 각 기능별로 사용하려면 메소드로 나누면 안 될 것 같다
그럼 if문으로 분기하고 DeptLogic.java에서 메소드를 분리해 보자
DeptLogic.java - 트랜잭션 처리(묶음배송-모든 요소가 있어야 전달, 하나의 프로세스)
DeptDao.java - DB연동만 전담/orm솔루션사용
<pojo step 1 - DeptController.java>
package com.pojo.step1;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
//SELECT deptno, dname, loc FROM dept
public class DeptController implements Action {
Logger logger = Logger.getLogger(DeptController.class);
// Action 인터페이스 상속 후 오버라이드한 메소드, 반환값이 ActionForward
@Override
public ActionForward execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ActionForward af = new ActionForward();
String upmu[] = (String[]) req.getAttribute("upmu"); // FrontMVC1에서 가져온 값
DeptLogic deptLogic = new DeptLogic();
String path = null;
boolean isRedirect = false;
// 부서목록 조회인 경우 - http://localhost:9000/dept/getDeptList.st1
if ("getDeptList".equals(upmu[1])) {
List<Map<String, Object>> deptList = deptLogic.getDeptList();
req.setAttribute("deptList", deptList);
path = "getDeptList.jsp";
isRedirect = false; // false이면 forward이다 - 요청이 유지된다, 주소창은 그대로 페이지만 바뀜
}
// json 부서목록 조회인 경우
else if ("jsonDeptList".equals(upmu[1])) {
String jsonDoc = deptLogic.jsonDeptList();
req.setAttribute("jsonDoc", jsonDoc);
path = "jsonDeptList.jsp";
isRedirect = false; // false이면 forward이다 - 요청이 유지된다, 주소창은 그대로 페이지만 바뀜
}
// 부서 등록인 경우
else if ("deptInsert".equals(upmu[1])) {
// insert into dept (deptno, dname, loc) values(?, ?, ?)
int result = deptLogic.deptInsert();
}
// 부서 정보 수정인 경우
else if ("deptUpdate".equals(upmu[1])) {
int result = deptLogic.deptUpdate();
}
// 부서 삭제인 경우
else if ("deptDelete".equals(upmu[1])) {
int result = deptLogic.deptDelete();
}
af.setPath(path);
af.setRedirect(isRedirect);
return af;
}
public ActionForward getDeptList() {
// 이렇게 메소드를 만들면 req, res가 없음
// res가 없으면 res.sendRedirect();를 못함! -> res에 의존적
return null;
}
}
<pojo step 1 - DeptLogic.java>
package com.pojo.step1;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import com.google.gson.Gson;
public class DeptLogic {
Logger logger = Logger.getLogger(DeptLogic.class);
public List<Map<String, Object>> getDeptList() {
logger.info("getDeptList 호출");
List<Map<String, Object>> deptList = new ArrayList<>();
Map<String, Object> rMap = new HashMap<>();
rMap.put("deptno", 10);
rMap.put("dname", "개발부");
rMap.put("loc", "부산");
deptList.add(rMap);
rMap = new HashMap<>();
rMap.put("deptno", 20);
rMap.put("dname", "운영부");
rMap.put("loc", "서울");
deptList.add(rMap);
rMap = new HashMap<>();
rMap.put("deptno", 30);
rMap.put("dname", "총무부");
rMap.put("loc", "대전");
deptList.add(rMap);
return deptList;
}
public String jsonDeptList() {
logger.info("jsonDeptList 호출");
List<Map<String, Object>> deptList = new ArrayList<>();
Map<String, Object> rMap = new HashMap<>();
rMap.put("deptno", 10);
rMap.put("dname", "개발부");
rMap.put("loc", "부산");
deptList.add(rMap);
rMap = new HashMap<>();
rMap.put("deptno", 20);
rMap.put("dname", "운영부");
rMap.put("loc", "서울");
deptList.add(rMap);
rMap = new HashMap<>();
rMap.put("deptno", 30);
rMap.put("dname", "총무부");
rMap.put("loc", "대전");
deptList.add(rMap);
Gson g = new Gson();
String temp = g.toJson(deptList);
return temp; // JSON 포맷으로 전달 -> 리액트에서 조회시에 사용함
}
public int deptInsert() {
logger.info("deptInsert 호출");
return 0;
}
public int deptUpdate() {
logger.info("deptUpdate 호출");
return 0;
}
public int deptDelete() {
logger.info("deptDelete 호출");
return 0;
}
}
<pojo step 1 - getDeptList.jsp>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.util.List, java.util.Map"%>
<%
List<Map<String, Object>> deptList = (List<Map<String, Object>>) request.getAttribute("deptList");
out.print(deptList);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>부서목록</title>
</head>
<body>
</body>
</html>
<pojo step 1 - jsonDeptList.jsp>
<%@ page language="java" contentType="application/json; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page trimDirectiveWhitespaces="true"%>
<%
String jsonDoc = (String) request.getAttribute("jsonDoc");
out.print(jsonDoc);
%>
<리액트 톰캣 연결 - dbLogic.js>
import axios from "axios";
export const jsonDeptList = (params) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_CHAT_BANANA_IP + "dept/jsonDeptList.st1",
params: params,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
/* rafce로 자동완성 단축키 - arrow function export default */
리액트와 서블릿 연동하기 - chat_banana[D:\workspace_java\chat_banana]
: yarn 패키지 매니저 사용함
설치 시 -g를 주면 전역에서 사용가능하게 설치
(-D는 개발자 의존성 주입: spring maven, gradle도 동일)
npm install -g yarn
패키지매니저 버전확인
yarn -version
리액트 프로젝트 생성하기
1. yarn create react-app template2023
단, 현재 바라보는 폴더 아래에 template2023 폴더가 생성된다
VSCode로 프로젝트 열 때도 위 경로에서 열어야 에러가 없음
지금 이 프로젝트는 D:\workspace_java\chat_banana\src\main에서 생성함
2. yarn init --force
그냥 계속 엔터 하면 디폴트값으로 package.json에 등록됨
3. react-router-dom 설치 -> 웹페이지 제작에 필수(화면전환 역할 - 링크도 포함)
yarn add react-router-dom
index.js에 아래와 같이 BrowserRouter를 추가해야 한다 - 단 App.jsx를 감싸준다
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
4. CSS 추가분
React-BootStrap
yarn add react-bootstrap
만약 React-BootStrap에 미지원 부분에 대해서는 BootStrap을 설치하여 사용자정의 CSS로 처리
yarn add bootstrap -> css라이브러리 설치 시
CSS를 자바스크립트 문법으로 처리해 주는 styled-components
yarn add styled-components
5. 폰트어썸 무료 이모티콘, 이모지
yarn add @fortawesome/fontawesome-free
6. axios 설치 - 비동기 처리 -> 톰캣서버의 서블릿 요청함
yarn add axios
fetch API에서 지원 안 되는 json포맷 지원받음
Moesif CORS - true/false
주의: CORS이슈
import axios from "axios";
export const jsonDeptList = (params) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_CHAT_BANANA_IP + "dept/jsonDeptList.st1",
params: params,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
/* rafce로 자동완성 단축키 - arrow function export default */
7. firebase 설치 - 9.17.1
yarn add firebase
로그인 인증
회원가입 지원
RealtimeDatabaseServer - 채팅기능 활용(30일)
storage - 이미지, 동영상(30일)
8. .env 사용
구글 키값, 카카오 키값 관리용도
.gitignore에 등록할 것
9. 실행하기
yarn start
http://localhost:3000/index.html 실행됨 -> div id="root"
사용자정의태그 Paint와 Nice도 사용가능
ReactDOM.createRoot(rootElement).render(element(<Paint/> <Nice/>))
index.html -> index.js -> BrowserRouter로 App.jsx를 감싸줌 -> App.jsx에서 Route 통해 페이지 링크
<리액트 BrowserRouter 테스트 - index.js>
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
<리액트 BrowserRouter 테스트 - App.jsx>
import { Route, Routes } from 'react-router-dom';
import './App.css';
import DeptPage from './components/page/DeptPage';
import EmpPage from './components/page/EmpPage';
import HomePage from './components/page/HomePage';
function App() {
return (
<>
<Routes>
<Route path="/" exact={true} element={<HomePage />} />
<Route path="/dept/:id" exact={true} element={<DeptPage />} />
<Route path="/emp" exact={true} element={<EmpPage />} />
</Routes>
</>
);
}
export default App;
<리액트 BrowserRouter 테스트 - Header.jsx>
import React from 'react'
const Header = () => {
return (
<div>
Header
</div>
)
}
export default Header
<리액트 BrowserRouter 테스트 - Bottom.jsx>
import React from 'react'
const Bottom = () => {
return (
<div>
Bottom
</div>
)
}
export default Bottom
<리액트 BrowserRouter 테스트 - HomePage.jsx>
import React from 'react'
const HomePage = () => {
return (
<div>
HomePage 페이지
</div>
)
}
export default HomePage
<리액트 BrowserRouter 테스트 - DeptPage.jsx>
import React from 'react'
import Bottom from '../include/Bottom'
import Header from '../include/Header'
const DeptPage = () => {
return (
<>
<Header />
<div>부서관리 페이지</div>
<Bottom />
</>
)
}
export default DeptPage
<리액트 BrowserRouter 테스트 - EmpPage.jsp>
import React from 'react'
import Bottom from '../include/Bottom'
import Header from '../include/Header'
const EmpPage = () => {
return (
<React.Fragment>
<Header />
사원관리시스템
<Bottom />
</React.Fragment>
)
}
export default EmpPage
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day63 (0) | 2023.02.27 |
---|---|
국비 지원 개발자 과정_Day62 (2) | 2023.02.24 |
국비 지원 개발자 과정_Day60 (0) | 2023.02.22 |
국비 지원 개발자 과정_Day59 (0) | 2023.02.21 |
국비 지원 개발자 과정_Day58 (0) | 2023.02.20 |
댓글