✿∘˚˳°∘°

77일차 : SpringMVC - 게시판2 본문

국비수업/Spring

77일차 : SpringMVC - 게시판2

_HYE_ 2023. 3. 17. 17:03

20230317

 

어제 작성했던 파일네이밍을 클래스의 메소드로 따로빼서 사용할것 (다른데에서도 똑같이 사용하기 때문에)

파일 업로드시 board notice 둘다 파일업로드를 한다고 가정하면 달라지는 부분은 파일이 올라가는 경로+실제파일 
-> 공통적인 부분을 따로 만들어서 매개변수를 받아 달라지는 부분만 수정(모듈화)

 

FileManager.java

@Component //객체생성을 위한것 - servlet-context.xml의 component-scan과 함께 이루어져야한다.
public class FileManager {
	//파일업로드를 위한 메소드
	public String upload(String savePath, MultipartFile file) {
		String filename = file.getOriginalFilename();
		String onlyFilename = filename.substring(0, filename.lastIndexOf("."));
		String extention = filename.substring(filename.lastIndexOf(".")); 
		String filepath = null;
		int count = 0; 
		while(true) {
			if(count == 0) {
				filepath = onlyFilename+extention;				
			}else {
				filepath = onlyFilename+"_"+count+extention;	
			}
			File checkFile = new File(savePath+filepath);
			if(!checkFile.exists()) {
				break;
			}
			count++;
		}
		try {
			FileOutputStream fos = new FileOutputStream(savePath+filepath);
			BufferedOutputStream bos = new BufferedOutputStream(fos);
			byte[] bytes = file.getBytes();
			bos.write(bytes);
			bos.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//filepath를 return을 String으로 잡아준것
		return filepath;
	}
}

- servlet-context.xml에서 component-scan을 잊지말자

	<context:component-scan base-package="common" />

 

1. 수정된 글작성하기(INSERT)

FileManager를 사용하기위해 상단에 선언

	@Autowired
	private FileManager fileManager;

 

Controller

	
	@RequestMapping(value="/boardWrite.do")
	//파일업로드를 진행하기위해 request를 불러옴
	public String boardWrite(Board b, MultipartFile[] boardFile, HttpServletRequest request) {
		//파일목록을 저장할 list생성
		ArrayList<FileVO> fileList = new ArrayList<FileVO>();
		//제목,내용,작성자는 매개변수 b에 모두 들어있음
		//파일이름은 b에 들어있지X 매개변수로 받음 -> 여러개를 받을거기때문에 자료형은 배열형태로
		//첨부파일은 input type="file"의 name으로 매개변수를 생성 -> 첨부파일 갯수만큼 배열길이 생성
		//첨부파일을 첨부하지 않아도 배열은 길이가 1
		//System.out.println(b);
		//System.out.println(boardFile.length);
		//첨부파일 존재유무를 구분하는 법 - 배열의 첫번째가 비어있는지 아닌지 확인
		//if(!boardFile[0].isEmpty()) {} if문은 이렇게 작성해주는게 좋다.
		if(boardFile[0].isEmpty()) {
			//첨부파일이 없는경우 진행할 로직이 없음
		}else {
			//첨부파일이 있는경우 파일업로드작업 수행
			//1. 파일업로드 경로 설정
			//getRealPath까지가 webapp
			String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
			//2. 배열이므로 반복문을 이용해서 파일업로드 처리
			for(MultipartFile file : boardFile) {
				//파일명이 기존업로드한 파일명과 중복되면 기존파일을 삭제하고 새파일로 덮어쓰기됨 -> 파일명이 중복되지않게 처리해야 함
				//(cos라이브러리에서는 new DefaultFileRenamePolicy가 파일이름이 중복일 경우 새로운이름을 붙여줬음 name1 name2)
				String filename = file.getOriginalFilename(); //사용자가 업로드한 이름
				//이걸 그대로 업로드하면 파일명이 중복되엇을 때 문제가 발생
				//가정 : filename => test.txt인데 중복인 경우 => test_1.txt 확장자기준 앞을 바꿈
				String filepath = fileManager.upload(savePath, file);
				
				
				/*
				//우선 text    .txt를 분리함
				String onlyFilename = filename.substring(0, filename.lastIndexOf("."));//test 0부터 뒤에서부터 .의 위치의 숫자를세서 그만큼까지
				String extention = filename.substring(filename.lastIndexOf(".")); //.txt 매개변수를 하나만주면 그 매개변수부터 끝까지라는 의미
				//실제 업로드할 파일명
				String filepath = null;
				//파일명 중복 시 뒤에 붙일 숫자
				int count = 0; //횟수가 정해져있지 않음 > while문을 무한으로 돌리고 중복이아닐때 빠져나옴
				while(true) {
					if(count == 0) {
						//첫번째 검증인 경우 숫자를 붙이지X
						filepath = onlyFilename+extention;				//test.txt
					}else {
						filepath = onlyFilename+"_"+count+extention;	//test_1.txt
					}
					File checkFile = new File(savePath+filepath);
					if(!checkFile.exists()) {
						//checkFile이 존재하는지 물어보는 것 -> 중복이 아닐 때 break 
						break;
					}
					count++;
				}
				//파일명 중복체크 끝 -> 업로드할 파일명 확정 -> 파일업로드진행
				//2-2. 중복처리가 끝난 파일 업로드
				try {
					//파일업로드를 위한 주스트림생성
					FileOutputStream fos = new FileOutputStream(savePath+filepath);
					//성능향상을 위한 보조스트림 생성
					BufferedOutputStream bos = new BufferedOutputStream(fos);
					//파일업로드
					byte[] bytes = file.getBytes();
					bos.write(bytes);
					bos.close();
				} catch (FileNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} */
				
				
				
				//파일업로드 끝(1개)
				//DB에 저장하기 위해 FileVO형태의 객체를 생성해서 fileList추가
				FileVO fileVO = new FileVO();
				fileVO.setFilename(filename);
				fileVO.setFilepath(filepath);
				fileList.add(fileVO);
			}//for문 끝
		}
		//비즈니스로직(board와 file_tbl에 insert진행)
		int result = service.insertBoard(b, fileList);
		if(result == (fileList.size()+1)) {
			//insert가 총 첨부파일+1개만큼 일어남 -> board인서트(1번) + 첨부파일인서트(여러번)
			//result를 누계했으므로 
			return "redirect:/boardList.do"; //컨트롤러를 거쳐가지않으면 리스트내용이 보이지않음
		}else {
			return "redirect:/";
		}
	}

- 주석 제외 Controller

	
	@RequestMapping(value="/boardWrite.do")
	public String boardWrite(Board b, MultipartFile[] boardFile, HttpServletRequest request) {
		ArrayList<FileVO> fileList = new ArrayList<FileVO>();
		if(boardFile[0].isEmpty()) {
		}else {
			String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
			for(MultipartFile file : boardFile) {
				String filename = file.getOriginalFilename(); //사용자가 업로드한 이름
				String filepath = fileManager.upload(savePath, file);	
				FileVO fileVO = new FileVO();
				fileVO.setFilename(filename);
				fileVO.setFilepath(filepath);
				fileList.add(fileVO);
			}
		}
		int result = service.insertBoard(b, fileList);
		if(result == (fileList.size()+1)) {
			return "redirect:/boardList.do"; 
		}else {
			return "redirect:/";
		}
	}

Service + Dao는 변화없음

 

2. 게시글 전체 목록(BoardList)

Controller

	@RequestMapping(value="/boardList.do")
	public String boardList(Model model) {
		ArrayList<Board> list = service.selectBoardList();
		model.addAttribute("list", list);
		return "board/boardList";
	}

Service

	public ArrayList<Board> selectBoardList() {
		ArrayList<Board> list = dao.selectBoardList();
		return list;
	}

Dao

	public ArrayList<Board> selectBoardList() {
		String query = "select board_no, board_title, board_writer, board_date from board order by 1 desc";
		List list = jdbc.query(query, new BoardListRowMapper());
		return (ArrayList<Board>)list;
	}

List를 확인할때만 사용할 RowMapper - BoardListRowMapper.java( boardContent 가 없는걸 확인)

-> RowMapper는 필요한 만큼 생성하면 된다.

package kr.or.board.model.vo;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class BoardListRowMapper implements RowMapper{
	
	@Override
	public Object mapRow(ResultSet rset, int rowNum) throws SQLException {
		Board b = new Board();
		b.setBoardNo(rset.getInt("board_no"));
		b.setBoardTitle(rset.getString("board_title"));
		b.setBoardWriter(rset.getString("board_writer"));
		b.setBoardDate(rset.getString("board_date"));
		return b;
	}
}

화면구현 JSP

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>게시물 목록</h1>
	<hr>
	<c:if test="${not empty sessionScope.m}">
		<h3><a href="/boardWriteFrm.do">게시글 작성하기</a></h3>
	</c:if>
	<table border="1">
		<tr>
			<th>글번호</th>
			<th>제목</th>
			<th>작성자</th>
			<th>작성일</th>
		</tr>
		<c:forEach items="${list }" var="b">
			<tr>
				<td>${b.boardNo }</td>
				<td><a href="/boardView.do?boardNo=${b.boardNo}">${b.boardTitle }</a></td>
				<td>${b.boardWriter }</td>
				<td>${b.boardDate }</td>
			</tr>
		</c:forEach>
	</table>
	<a href="/">메인으로</a>
</body>
</html>

 

3. 게시글 상세보기(BoardView)

Controller

	@RequestMapping(value="/boardView.do")
	public String boardView(int boardNo, Model model) {
		Board b = service.selectOneBoard(boardNo);
		if(b != null) {
			model.addAttribute("b", b);
			return "board/boardView";			
		}else {
			return "redirect:/boardList.do";
		}
	}

Service

	public Board selectOneBoard(int boardNo) {
		//보드 조회
		Board b = dao.selectOneBoard(boardNo);
		//파일 조회
		if(b != null) {
			ArrayList<FileVO> fileList = dao.selectFileList(boardNo);
			b.setFileList(fileList);
		}
		return b;
	}

Dao 1 : Board 테이블 조회

	public Board selectOneBoard(int boardNo) {
		String query = "select * from board where board_no = ?";
		Object[] params = {boardNo};
		//이번엔 content가 있기 때문에 BoardListRowMapper는 사용할 수 없음(boardContent가 없음)
		List list = jdbc.query(query, params, new BoardRowMapper());
		if(list.isEmpty()) {
			return null;
		}else {
			return (Board)list.get(0);		
		}
	}

Dao 2 : File_tbl 테이블 조회

	public ArrayList<FileVO> selectFileList(int boardNo) {
		String query = "select * from file_tbl where board_no = ?";
		Object[] params = {boardNo};
		List list = jdbc.query(query, params, new FileRowMapper());
		return (ArrayList<FileVO>)list;
	}

파일도 전부 조회해서 Board VO에 있는 fileList에 set으로 넣어준다.

 

화면구현 JSP

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>게시물 상세보기</h1>
	<hr>
	<table border="1">
		<tr>
			<th>글번호</th>
			<td>${b.boardNo }</td>
			<th>제목</th>
			<td>${b.boardTitle }</td>
			<th>작성자</th>
			<td>${b.boardWriter }</td>
			<th>작성일</th>
			<td>${b.boardDate }</td>
		</tr>
		<tr>
			<th>첨부파일</th>
			<td colspan="7">
				<%--첨부파일은 여러개일 수 있으므로 for문을 돌림 --%>
				<c:forEach items="${b.fileList }" var="f">
					<a href="/boardFileDown.do?fileNo=${f.fileNo }">${f.filename }</a>
				</c:forEach>
			</td>
		</tr>
		<tr>
			<th>내용</th>
			<td colspan="7">${b.boardContent }</td>
		</tr>
	</table>
	<c:if test="${not empty sessionScope.m && sessionScope.m.memberId eq b.boardWriter }">
			<a href="/boardUpdateFrm.do?boardNo=${b.boardNo }">수정</a>
			<a href="/boardDelete.do?boardNo=${b.boardNo }">삭제</a>
	</c:if>
</body>
</html>

4. 파일다운로드

Controller

	@RequestMapping(value="/boardFileDown.do")
	public void boardFileDown(int fileNo, HttpServletRequest request, HttpServletResponse response) {
		//fileNo : DB에서 filename, filepath를 조회해오기위한 용도
		//request : 파일위치 찾을 때 사용
		//response : 파일다운로드 로직 구현 시 사용
		//리턴을 하지않음 - 페이지이동이 필요없으므로
		
		//filename과 filepath를 찾아오기위해서
		FileVO file = service.getFile(fileNo);
		
		//파일경로
		String root = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
		String downFile = root+file.getFilepath();
		
		//파일을 읽어오기위한 주스트림생성(속도개선을위한 보조스트림생성)
		try {
			FileInputStream fis = new FileInputStream(downFile);
			BufferedInputStream bis = new BufferedInputStream(fis);
			//읽어온 파일을 사용자에게 내보낼 스트림생성
			ServletOutputStream sos = response.getOutputStream();
			BufferedOutputStream bos = new BufferedOutputStream(sos);
			
			//파일명 처리
			String resFilename = new String(file.getFilename().getBytes("UTF-8"), "ISO-8859-1");
			response.setContentType("application/octet-stream");//파일형식이란것을 알려줌
			response.setHeader("Content-Disposition", "attachment;filename="+resFilename);//파일이름을 알려줌
			//파일전송
			while(true) {
				int read = bis.read();
				//파일을 계속 읽다가 다읽으면 종료
				if(read != -1) {
					bos.write(read);
				}else {
					break;
				}
			}
			bos.close();
			bis.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

Service(파일path와 name을 찾기위한 로직)

	public FileVO getFile(int fileNo) {
		return dao.getFile(fileNo);
	}

Dao

	public FileVO getFile(int fileNo) {
		String query = "select * from file_tbl where file_no = ?";
		Object[] params = {fileNo};
		List list = jdbc.query(query, params, new FileRowMapper());
		return (FileVO)list.get(0);
	}

 

5. 게시글 삭제( deleteBoard )

Controller

	@RequestMapping(value="/boardDelete.do")
	public String boardDelete(int boardNo, HttpServletRequest request) {
		//int result = service.deleteBoard(boardNo);
		//원래는 int로 받는게 맞지만 file또한 지워야 하기 때문에 file목록을 가져올것
		//DB를 삭제하고, 서버에 업로드 되어있는 파일을 지우기 위해서 파일목록을 가져옴
		ArrayList<FileVO> list = service.deleteBoard(boardNo);
		//list가 null이면 실패, 아니면 성공
		if(list == null) {
			return "redirect:/boardView.do?boardNo="+boardNo;
		}else {
			//성공 시 실제 file을 삭제
			String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
			for(FileVO file : list) {
				boolean deleteResult = fileManager.deleteFile(savePath, file.getFilepath());
				if(deleteResult) {
					System.out.println("파일삭제성공");
				}else {
					System.out.println("파일삭제실패");
				}
			}
			return "redirect:/boardList.do";
		}
	}

Service - 파일또한 삭제해줘야 하기 때문에 파일먼저 조회 후 게시글 삭제진행

	public ArrayList<FileVO> deleteBoard(int boardNo) {
		//삭제이후에 조회를 하면 on delete cascade 때문에 이미 지워진 상태이기 때문에 확인불가
		ArrayList<FileVO> fileList = dao.selectFileList(boardNo);
		int result = dao.deleteBoard(boardNo);
		if(result > 0) {
			return fileList;
		}else {
			return null;
		}
	}

Dao 1 : 파일목록 조회

	public ArrayList<FileVO> selectFileList(int boardNo) {
		String query = "select * from file_tbl where board_no = ?";
		Object[] params = {boardNo};
		List list = jdbc.query(query, params, new FileRowMapper());
		return (ArrayList<FileVO>)list;
	}

Dao 2 : 게시글 삭제

	public int deleteBoard(int boardNo) {
		String query = "delete from board where board_no = ?";
		Object[] params = {boardNo};
		int result = jdbc.update(query, params);
		return result;
	}

 

6. 게시글 수정(BoardUpdate)

- 기존 게시글 정보를 가지고 수정폼으로 이동

	@RequestMapping(value="/boardUpdateFrm.do")
	public String boardUpdateFrm(int boardNo, Model model) {
		Board b = service.selectOneBoard(boardNo);
		model.addAttribute("b", b);
		return "board/BoardUpdateFrm";
	}

수정폼JSP

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
	<h1>게시글 수정</h1>
	<hr>
	<form action="/boardUpdate.do" method="post" enctype="multipart/form-data" id="updateFrm">
		<table border="1">
			<tr>
				<th>글번호</th>
				<td>
					<input type="hidden" name="boardNo" value="${b.boardNo }">
					${b.boardNo }
				</td> 
			</tr>
			<tr>
				<th>제목</th>
				<td>
					<input type="text" name="boardTitle" value="${b.boardTitle }">
				</td>
			</tr>
			<tr>
				<th>작성자</th>
				<td>${b.boardWriter }</td>
			</tr>
			<tr>
				<th>작성일</th>
				<td>${b.boardDate }</td>
			</tr>
			<tr>
				<th>첨부파일</th>
				<td>
					<c:forEach items="${b.fileList }" var="f">
						<p>
							${f.filename }
							<button type="button" onclick="deleteFile(this, ${f.fileNo}, '${f.filepath }');">삭제</button>
						</p>
					</c:forEach>
				</td>
			</tr>
			<tr>
				<th>첨부파일 추가</th>
				<td>
					<input type="file" name="boardFile" multiple>
				</td>
			</tr>
			<tr>
				<th>내용</th>
				<td>
					<textarea name="boardContent">${b.boardContent }</textarea>
				</td>
			</tr>
			<tr>
				<th colspan="2"><input type="submit" value="수정하기"></th>
			</tr>
		</table>	
	</form>
	<script>
		function deleteFile(obj, fileNo, filepath){
			//화면에서 파일을 안보여주게 하기위해 obj(this)를 가져옴 -> p태그를 지워줄것
			//file인풋을 만들어서 form에 넣어줄것 -> DB삭제를 위한 fileNo / 파일삭제를 위한 filepath
			//<input>
			const fileNoInput = $("<input>");
			//<input name="fileNo">
			fileNoInput.attr("name", "fileNo");
			//<input name="fileNo" value="글번호숫자">
			fileNoInput.val(fileNo);
			//<input name="fileNo" value="글번호숫자" style="display:none;">
			fileNoInput.hide();
			
			const filepathInput = $("<input>");
			filepathInput.attr("name", "filepath");
			filepathInput.val(filepath);
			filepathInput.hide();
			
			$("#updateFrm").append(fileNoInput).append(filepathInput);
			$(obj).parent().remove();
		}
	</script>
</body>
</html>

수정 Controller

	@RequestMapping(value="/boardUpdate.do")
	public String boardUpdate(Board b, int[] fileNo, String[] filepath, MultipartFile[] boardFile, HttpServletRequest request) {
		//fileVO file로 받으면 문제점 발생 -> 하나만 지우면 fileVO로 받으면 되지만 여러개이기 때문에 각각 배열을 받아야함 
		//b -> boardNo, boardTitle, boardContent
		//fileNo : 삭제할 파일번호(DB) / filepath : 삭제할 파일이름(물리적으로 파일폴더에서삭제) 
		//boardFile : 새로올릴 파일 / request : 새파일 경로잡기위함
		//file -> fileNo, filepath 
		//첨부파일이 추가로 들어왔으면 INSERT
		ArrayList<FileVO> fileList = new ArrayList<FileVO>();
		String savePath = request.getSession().getServletContext().getRealPath("/resources/upload/board/");
		if(!boardFile[0].isEmpty()) {
			for(MultipartFile file : boardFile ) {
				String filename = file.getOriginalFilename();
				String upFilepath = fileManager.upload(savePath, file);
				FileVO fileVO = new FileVO();
				fileVO.setFilename(filename);
				fileVO.setFilepath(upFilepath);
				fileList.add(fileVO);
				//파일업로드 작업 완료
			}
		}
		int result = service.boardUpdate(b, fileList, fileNo);
		//업데이트 성공 조건 : result == 삭제한 파일수 + 추가한파일수 + 1
		if(fileNo != null && (result == (fileList.size() + fileNo.length + 1))) {
			//삭제한 파일이 있는 상태에서 모든 로직을 성공 했을 때
			//실제파일 지우기
			for(String delFile : filepath) {
				boolean delResult = fileManager.deleteFile(savePath, delFile);
				if(delResult) {
					System.out.println("삭제성공");
				}else {
					System.out.println("삭제실패");
				}
			}
			return "redirect:/boardView.do?boardNo="+b.getBoardNo();
		}else if(fileNo == null && (result == fileList.size()+1)) {
			//삭제한 파일이 없는 상태에서 모든 로직을 성공 했을 때
			return "redirect:/boardView.do?boardNo="+b.getBoardNo();
		}else {
			//실패
			return "redirect:/boardList.do";
		}
	}

Service

	public int boardUpdate(Board b, ArrayList<FileVO> fileList, int[] fileNo) {
		//1. board테이블 수정
		int result = dao.updateBoard(b);
		if(result > 0) {
			//2. 기존 첨부파일 삭제
			//기존첨부파일을 삭제하지 않을 경우 -> 추가한 input이 없음 -> int[] fileNo 가 null
			if(fileNo != null) {
				//삭제할 첨부파일이 있는경우
				for(int no : fileNo) {
					result += dao.deleteFile(no);
				}
			}
			//3. 새로운 첨부파일이 있으면 추가
			for(FileVO f : fileList) {
				f.setBoardNo(b.getBoardNo());
				result += dao.insertFile(f);
			}
		}
		return result;
	}

Dao 1 : 게시글 업데이트

	public int updateBoard(Board b) {
		String query = "update board set board_title=?, board_content=? where board_no =?";
		Object[] params = {b.getBoardTitle(), b.getBoardContent(), b.getBoardNo()};
		int result = jdbc.update(query, params);
		return result;
	}

Dao 2 : 삭제할 파일이 있으면 파일 삭제

	public int deleteFile(int no) {
		String query = "delete from file_tbl where file_no = ?";
		Object[] params = {no};
		int result = jdbc.update(query, params);
		return result;
	}

Dao 3 : 새로추가된 파일이 있으면 INSERT(기존 파일INSERT 이용)

	public int insertFile(FileVO file) {
		String query = "insert into file_tbl values(file_seq.nextval, ?, ?, ?)";
		Object[] params = {file.getBoardNo(), file.getFilename(), file.getFilepath()};
		int result = jdbc.update(query, params);
		return result;
	}

 

 

'국비수업 > Spring' 카테고리의 다른 글

79일차 : Dynamic Mybatis  (0) 2023.03.21
78일차 : Mybatis  (0) 2023.03.21
76일차 : 정규화, SpringMVC - 게시판  (0) 2023.03.16
75일차 : SpringMVC - 회원, 공지사항  (0) 2023.03.15
74일차 : loC, DI, springMVC  (0) 2023.03.14
Comments