Spring/Spring 공부

[spring] 파일 입출력

congs 2023. 6. 13. 15:13

1. fileUpload 라이브러리  ->  pom.xml에 추가

- 필요한 라이브러리

  1. commens-fileUpload 1.4
  2. commins-io 2.11.0
  3. thumbnailator 0.4.14
  4. tika-core 1.28 ( tika : 확장자 확인 용도 )
  5. tika-parsers 1.28
<!-- 파일 입출력 -->
		<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
		<dependency>
		    <groupId>commons-fileupload</groupId>
		    <artifactId>commons-fileupload</artifactId>
		    <version>1.4</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
		<dependency>
		    <groupId>commons-io</groupId>
		    <artifactId>commons-io</artifactId>
		    <version>2.11.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
		<dependency>
		    <groupId>net.coobird</groupId>
		    <artifactId>thumbnailator</artifactId>
		    <version>0.4.14</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
		<dependency>
		    <groupId>org.apache.tika</groupId>
		    <artifactId>tika-core</artifactId>
		    <version>1.28</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-parsers -->
		<dependency>
		    <groupId>org.apache.tika</groupId>
		    <artifactId>tika-parsers</artifactId>
		    <version>1.28</version>
		</dependency>

▲ tica-core, tica-parsers의 1.28 버전

▼ tica-core, tica-parsers의 2.4.1 버전

<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
		<dependency>
		    <groupId>org.apache.tika</groupId>
		    <artifactId>tika-core</artifactId>
		    <version>2.4.1</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-parsers -->
		<dependency>
		    <groupId>org.apache.tika</groupId>
		    <artifactId>tika-parsers</artifactId>
		    <version>2.4.1</version>
		    <type>pom</type>
		</dependency>

추가하고, 다른 설정에 영향을 미치는 값이 있는지 생각해보고 넘어가기~!


 

2. file 경로 설정 xml 추가

1 ) 파일을 업로드 할 경로를 외부에 설정 ( 폴더를 따로 생성하기 )

새로 생성한 폴더안의 폴더안의 폴더

경로 : D:\_myweb\_java\_fileUpload

 

2 ) web.xml에 파일 경로 및 설정 설정

  • 위치 : appServlet의 <servlet>태그 안, <load-on-startup>태그 아래
web.xml 파일 설정시 추가 설정

1. 파일의 최대 크기 설정 : 1M - 2M
2. 요청에 따른 응답(request) 크기 설정 : 4M - 5M -10M
3. file크기 여분 메모리 size 설정 : 1M - 2M
 ( M = mega , 1M = 1*1024*1024 , 2M = 2*1024*1024 )
 ( 20971520 = 20M )

- 추가 부분

<multipart-config>
			<location>D:\_myweb\_java\_fileUpload</location>
			<max-file-size>20971520</max-file-size>
			<max-request-size>41943040</max-request-size>
			<file-size-threshold>20971520</file-size-threshold>
</multipart-config>

- 추가 후 전체

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
			/WEB-INF/spring/appServlet/servlet-context.xml
			/WEB-INF/spring/appServlet/security-context.xml
			</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
		<multipart-config>
			<location>D:\_myweb\_java\_fileUpload</location>
			<max-file-size>20971520</max-file-size>
			<max-request-size>41943040</max-request-size>
			<file-size-threshold>20971520</file-size-threshold>
		</multipart-config>
	</servlet>
		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<filter>
		<filter-name>encoding</filter-name>
		<filter-class>
			org.springframework.web.filter.CharacterEncodingFilter
		</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<filter-mappring>
			<filter-name>encoding</filter-name>
			<url-pattern>/*</url-pattern>
		</filter-mappring>
	</filter>

</web-app>

 

3 ) servlet-context.xml에 파일 경로 맵핑, multipartResolver bean 설정

- 추가 부분

- 파일 경로 맵핑
<resources location="file:///D:\_myweb\_java\_fileUpload\" mapping="/upload/**" />

- multipartResolver bean 설정
<beans:bean id="mutipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</beans:bean>

- 추가 후 전체

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />
	<resources location="file:///D:\_myweb\_java\_fileUpload\" mapping="/upload/**" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!-- 자동으로 경로를 지정 -->
		<beans:property name="prefix" value="/WEB-INF/views/" /><!-- 해당경로 앞에 붙여서 기본적으로 타고 들어가도록 -->
		<beans:property name="suffix" value=".jsp" /><!-- 경로의 뒤에 붙여서 jsp파일이 되도록 -->
	</beans:bean>
	
	<context:component-scan base-package="com.myweb.www" />
	
	<beans:bean id="mutipartResolver"
	class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
	</beans:bean>
	
	
</beans:beans>

 

 


 

3. DB 생성 ) file을 별도로 나타낼 수 있는 table 생성

uuid(범용 고유 식별자) : random한 고유 번호를 설정

create table file(
uuid varchar(256) not null,
save_dir varchar(256) not null,
file_name varchar(256) not null,
file_type tinyint(1) default 0,
bno int,
file_size int,
reg_date datetime default now(),
primary key(uuid)
);


webapp - resource - sql에 구문 추가

 


 

4. domain패기지에 FileVO ( class 파일 ) 생성

package com.myweb.www.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@ToString
@Setter
@Getter
public class FileVO {

	/*
	 * create table file(
		uuid varchar(256) not null,
		save_dir varchar(256) not null,
		file_name varchar(256) not null,
		file_type tinyint(1) default 0,
		bno int,
		file_size int,
		reg_date datetime default now(),
		primary key(uuid)
		);
	 * */
	
	private String uuid;
	private String save_dir;
	private String file_name;
	private int file_type;
	private int bno;
	private int file_size;
	private String reg_date;
	
	
}

 


 

5. board폴더의 register.jsp에 파일 부분 추가

- 추가 부분

<form action="/board/register" method="post" enctype="mutipart/form-data">

...

file : <input type="file" name="file" multiple>
<!-- multiple : 다중파일 업로드 가능 (파일을 배열처리해서 여러개를 가져감) -->

<button type="button">fileUpload</button>
<!-- 파일에 대한 업로드가 가능하도록 설정하는 버튼 -->

<div id="filezone">
<!-- 파일 목록이 리스트로 올라가는 부분 -->


</div>

- 추가 후 전체 부분

<%@ 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>Register Page</title>
</head>
<body align="center">
   <h1>Register Page</h1>
   <form action="/board/register" method="post" enctype="multipart/form-data">
   title : <input type="text" name="title" placeholder="제목"> <br>
   writer : <input type="text" name="writer" value="${ses.id }"> <br>
   content : <br>
   <textarea rows="10" cols="50" name="content"></textarea><br>
   
   file : <input type="file" name="file" id="file" multiple>
   <!-- multiple : 다중파일 업로드 가능 (파일을 배열처리해서 여러개를 가져감) -->
   <button type="button" id="trigger">fileUpload</button> <br>
   <!-- 파일에 대한 업로드가 가능하도록 설정하는 버튼 -->
   <div id="filezone">
   <!-- 파일 목록이 리스트로 올라가는 부분 -->
   
   </div>
   
   <button type="submit">등록</button> <br>
   </form>
   <a href="/"><button type="button">home</button></a>
</body>

 

 


 

6.  webapp - resourse - js폴더에 boardRegister.js  생성

  • fileUpload버튼을 클릭하면, file이 클릭되는 효과 설정하기
  • 왜 따로 버튼을 만들고 연결해?  css꾸미기를 위해서! 

document.getElementById("trigger").addEventListener('click',()=>{
    document.getElementById('file').click();
})

7. board폴더의 register.jsp에 js연결코드 작성

- 추가

<script type="text/javascript" src="/resource/js/boardRegister.js"></script>

- 추가 후 전체

<%@ 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>Register Page</title>
</head>
<body align="center">
   <h1>Register Page</h1>
   <form action="/board/register" method="post" enctype="multipart/form-data">
   title : <input type="text" name="title" placeholder="제목"> <br>
   writer : <input type="text" name="writer" value="${ses.id }"> <br>
   content : <br>
   <textarea rows="10" cols="50" name="content"></textarea><br>
   
   file : <input type="file" name="file" id="file" multiple>
   <!-- multiple : 다중파일 업로드 가능 (파일을 배열처리해서 여러개를 가져감) -->
   <button type="button" id="trigger">fileUpload</button> <br>
   <!-- 파일에 대한 업로드가 가능하도록 설정하는 버튼 -->
   <div id="filezone">
   <!-- 파일 목록이 리스트로 올라가는 부분 -->
   
   </div>
   
   <button type="submit">등록</button> <br>
   </form>
   <a href="/"><button type="button">home</button></a>
   
   <script type="text/javascript" src="/resources/js/boardRegister.js">
   </script>
   
</body>

 


 

8. boardRegister.js 에 이미지파일 유무 설정하는 코드 작성

  • 상단 click연결 부분 제외 모두 작성 부분임


filezone에 생성할 모양

document.getElementById("trigger").addEventListener('click',()=>{
    document.getElementById('file').click();
})

// 정규표현식을 사용하여 생성자 함수를 만들기
// 실행파일을 막고, 이미지파일 유무 체크
//생성자함수의 시작은 \.
const regExp = new RegExp("\.(exe|sh|bat|msi|dll|js)$");  //들어오면 안되는 형식
const regExpImg = new RegExp("\.(jpg|jpeg|png|gif|bmp)$"); //이미지 파일 형식
const maxSize = 1024*1024*20; //20M

function fileSizeValidation(fileName, fileSize){
    if(regExp.test(fileName)){ //test : 맞는지 아닌지 확인! 맞으면 true (들어오면 안되는 형식의 경우)
        return 0;
    }else if(fileSize > maxSize){ //파일의 사이즈가 너무 큰 경우
        return 0;
    }else if(!regExpImg.test(fileName)){//이미지 파일이 아니면 첨부 X
        return 0;
    }else{
        return 1;
    }

}

//image file에 따라서 체크 (등록 가능 여부)
document.addEventListener('change',(e)=>{
    console.log("e.target : " + e.target);
    if(e.target.id == 'file'){
        document.getElementById('regBtn').disabled = false;//등록버튼 열기
        // 첨부되지 말하야하는 파일이 들어왔을 경우 전송되는 것을 방지 (등록버튼 사용 X)
       
        const fileObject = document.getElementById('file').file;
        console.log("fileObject : " + fileObject);

        //filezone
        let div = document.getElementById('filezone');
        div.innerHTML='';
        let ul = `<ul>`;
        let isOk = 1; //파일의 통과여부(fileSizeValidation) - 통과면 1
        for(let file of fileObject){
            let vaildResult = fileSizeValidation(file.name, file.size); //여러파일이 들어간 배열
            isOk *= vaildResult; //곱하기? 하나라도 아니면 0을 곱하면 0이니까(모든 파일이 1이여 통과)
            ul += `<li>`; //li태그 열고
            //업로드 가능 표시 작성 ( 1 = true )
            ul += `<div> ${vaildResult ? '업로드 가능' : '업로드 불가능'} </div>`;
            ul += `${file.name}`;
            ul += `<span>${file.size}Byte</span>`;
            ul += `</li>`;
        }
        ul += `</ul>`;
        div.innerHTML = ul;

        if(isOk == 0){ //첨부 불가파일이 있는 경우
            document.getElementById('regBtn').disabled = true; //등록막기

        }
    }
})

<ul>
    //첨부파일이 2개 이상인 경우, 이 위치에 for문을 돌려 li를 여러개 생성할 예정
    <li> //li하나당 하나의 파일
        <div>업로드 가능</div>
        <div>업로드 불가능</div>
        파일의 이름
        <span>파일 사이즈</span>
    </li>
</ul>

 


 

9. board-register.jsp의 등록 버튼에 id 생성

  • 파일이 맞다면 regBtn이 disabled=false하고 (등록버튼 클릭 가능)
  • 파일이 아니라면 regBtn이 disabled=true설정 (등록버튼 클릭 불가능)

<%@ 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>Register Page</title>
</head>
<body align="center">
   <h1>Register Page</h1>
   <form action="/board/register" method="post" enctype="multipart/form-data">
   title : <input type="text" name="title" placeholder="제목"> <br>
   writer : <input type="text" name="writer" value="${ses.id }"> <br>
   content : <br>
   <textarea rows="10" cols="50" name="content"></textarea><br>
   
   file : <input type="file" name="file" id="file" multiple>
   <!-- multiple : 다중파일 업로드 가능 (파일을 배열처리해서 여러개를 가져감) -->
   <button type="button" id="trigger">fileUpload</button> <br>
   <!-- 파일에 대한 업로드가 가능하도록 설정하는 버튼 -->
   <div id="filezone">
   <!-- 파일 목록이 리스트로 올라가는 부분 -->
   
   </div>
   
   <button type="submit" id="regBtn">등록</button> <br>
   </form>
   <a href="/"><button type="button">home</button></a>
   
   <script type="text/javascript" src="/resources/js/boardRegister.js">
   </script>
   
</body>

 

 == 여기까지 출력화면 ==

 


 


 

10. board-register.jsp의 파일 선택 버튼을 안보이게 변경

+ name을 files로 변경

file : <input type="file" name="files" id="file" multiple style="display:none">

 


 

11. BoardController의 registerPost메서드 파일 저장 구문 추가

 @PostMapping("/register")
	   public String registerPost(BoardVO bvo, Model m, RedirectAttributes rAttr, 
			   @RequestParam(name="files", required = false)MultipartFile[] files) { 
		   //RedirectAttributes : 컨트롤 안에서 이동이 가능하도록 하는 역할 + 휘발성 사용을 위해서 이용 (데이터의 새로고침)
		   //required : 필수여부 = 해당 param이 필수로 가져와야하는가 확인 ( false = 해당 파라미터가 없더라도 예외가 발생하지 않음 )
	      log.info(">>> bvo "+bvo.toString());
	      log.info("files : " + files.toString());
	      
	      List<FileVO> fList = null;
	      //file 처리 -> handler에서 만들어서 호출!
	      
	      
	      int isOk = bsv.register(bvo);
	      log.info(">>> board register >> "+( isOk > 0 ? "OK" : "Fail"));
	      rAttr.addFlashAttribute("isOk", isOk); //일회성으로 사용!
//	      return "redirect:/board/list";
//	      return "redirect:/member/login"; //이런 방법으로 같은 패키지의 타 controller로 이동도 가능
	      return "redirect:/"; 
	      // /는 홈으로 이동
	      // redirect사용 : insert, update, delete
	   }

12. 파일처리를 할 FileHandler ( class파일 ) Handler패키지에 생성

package com.myweb.www.Handler;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.apache.tika.Tika;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.myweb.www.domain.FileVO;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnailator;
import net.coobird.thumbnailator.Thumbnails;

@Slf4j
@AllArgsConstructor
@Component 
public class FileHandler {
	
	private static final Logger log = LoggerFactory.getLogger(FileHandler.class);
	private final String UP_DIR = "D:\\_myweb\\_java\\_fileUpload";
	
	public List<FileVO> uploadFiles(MultipartFile[] files){
		
		//---------------- 1. 가져온 파일들을 저장할 폴더를 생성 ----------------------
		
		//일자별로 파일을 정리할 예정 ( date )
		LocalDate date = LocalDate.now(); 
		log.info(">>> date : " + date);
		
		String today = date.toString(); //date객체를 string으로 변환 ( 2023-06-14 형태 ) 
		
		// 년도안의 월안의 일 폴더로 계층적 폴더를 만들예정 2023\06\14(window) , 2023/06/14(ios/linux)
		today = today.replace("-", File.separator);//today를 '-'기준으로 자르기
		//today로 폴더 구성 (경로 구성)
		File folders = new File(UP_DIR, today); 
		//폴더가 기존에 있다면 생성x, 없다면 생성
		if(!folders.exists()) {//폴더가 없는 경우
			folders.mkdirs(); //폴더 생성 명령어 (실제 폴더 생성)
			
		}
		//경로 설정
		List<FileVO> fList = new ArrayList<FileVO>();
		for(MultipartFile file : files) {
			
			// --------- fvo에 저장할 정보 생성 (set) ----------
			FileVO fvo = new FileVO();
			fvo.setSave_dir(today); //파일 경로 넣기
			fvo.setFile_size(file.getSize()); //파일 사이즈 설정
			
			//경로가 포함되어 있을 수도 있는 파일 명
			String originalFileName = file.getOriginalFilename();
			log.info("originalFileName :" + originalFileName);
			//실제 파일 명
			String onlyFileName = originalFileName.substring(
					originalFileName.lastIndexOf(File.separator)+1);
			log.info("onlyFileName : " + onlyFileName);
			//파일의 이름 설정
			fvo.setFile_name(onlyFileName);
			
			//UUID(java에서 기본 제공) 생성 + 설정
			UUID uuid = UUID.randomUUID();
			fvo.setUuid(uuid.toString());
			
			// --------- 디스크에 저장할 객체 생성 (저장) ----------
			//실제 저장할 full 파일이름
			String fullfileName = uuid.toString()+"_"+onlyFileName; 
			//폴더와 이름을 담아 생성
			File storeFile = new File(folders, fullfileName);
			
			try {
				//원본 객체를 저장할 위한 형태의 객체로 복사 
				file.transferTo(storeFile);
				//파일의 타입 결정
				if(isImgFile(storeFile)) { //이미지인 경우 
					//파일 타입을 1로 변경 (default값으로 0설정되어있음)
					fvo.setFile_type(1);
					//썸네일 설정
					//썸네일 파일의 경로
					File thumbNail = new File(folders, uuid.toString()+"_"+onlyFileName);
					//썸네일 생성
					Thumbnails.of(storeFile).size(75, 75).toFile(thumbNail);
				}
			} catch (Exception e) {
				log.info(">>> 파일 생성 오류 발생");
				e.printStackTrace();
			}
			//fList에 생성한 fvo넣고
			fList.add(fvo);
		}
		//fList리턴
		return fList;
		
		
		
	}
	//tika를 이용한 파일 형식 체크 메서드 (여기서만 사용할 예정이라 private)
	//이미지파일이 맞으면 true아니면 false 리턴
	private boolean isImgFile(File storeFile) throws IOException {
		//해당하는 이미지의 타입을 빼기 (image/jpg, image/png 모양의 string형태)
		String mimeType = new Tika().detect(storeFile); 
		log.info(mimeType);
		//startWith : 앞에 원하는 제시어 있는지 확인 (image가 앞에 있으면 tru리턴)
		return mimeType.startsWith("image")? true : false;
	}
	
	
}

13. BoardController에서 파일 처리구문 작성

  • controller 상단에 @Inject private FileHandler fhd; 로 FileHandler import!
  • registerPost메서드에 파일 처리 구문 작성
@Inject
	private FileHandler fhd;
 @PostMapping("/register")
	   public String registerPost(BoardVO bvo, Model m, RedirectAttributes rAttr, 
			   @RequestParam(name="files", required = false)MultipartFile[] files) { 
		   //RedirectAttributes : 컨트롤 안에서 이동이 가능하도록 하는 역할 + 휘발성 사용을 위해서 이용 (데이터의 새로고침)
		   //required : 필수여부 = 해당 param이 필수로 가져와야하는가 확인 ( false = 해당 파라미터가 없더라도 예외가 발생하지 않음 )
	      log.info(">>> bvo "+ bvo.toString());
	      log.info("files : " + files.toString());
	      
	      List<FileVO> fList = null;
	      //file 처리 -> handler에서 만들어서 호출!
	      if(files[0].getSize()>0) {//가져온 파일이 있는 경우
	    	  //fhd.uploadFiles(files) : 파일 배열을 경로설정, fvo set해서 리스트로 리턴
	    	  fList = fhd.uploadFiles(files);
	      }else {
	    	  log.info("file null 파일 없음");
	      }
	      //파일과 보드 처리를 같이할건지 별도로 할것인지 선택!
	   
	      
//	      int isOk = bsv.register(bvo);
	      log.info(">>> board register >> "+( isOk > 0 ? "OK" : "Fail"));
	      rAttr.addFlashAttribute("isOk", isOk);
	      return "redirect:/board/list"; 
	   }

14. file과 board bvo처리를 같이 할 BoardDTD생성

  1.