반응형

작성한 프로그램이 정상적으로 수행이 되고 메모리 낭비는 없는지? 생성한 Thread가 잘 동작하다가 문제없이 삭제되는지 등 여러가지 JAVA에서 생성된 스펙을 확인하고자 할때, JConsole을 통해 좀 더 유용하게 확인 할 수 있습니다.

 

 

Netty Client를 통해 여러 곳에 메시지를 처리해야 하는 상황이였는데, 이부분을 생성자를 통해 각각 필요할때마다 불러와서 new 생성자를 통해 처리하였습니다.

 

Netty에는 EventLoop가 Thread로 동작하게 되어 있는데, 생성된 이 group들을 별도로 종료하지 않으면 was가 종료되기 전까지 계속 살아있게되고 이상태로 서비스가 운영된다면 추후에는 Memory Leak과 같은 현상이 발생하면서 was가 죽어버리는 끔찍한 일이 발생할 수 있습니다.

 

JConsole

1. JConsole 실행하기

- 설치한 자바 위치의 bin디렉토리에 JConsole이 있습니다.

- 환경변수를 잡았다면 cmd창에 JConsole을 입력합니다.

JConsole입력

실행해줍니다.

 

2. JConsole 확인할 Java 프로세스 선택하기

JConsole이 동작하고 확인할 java프로세스를 선택하라고 뜹니다.

여기서 확인하고 싶은 클릭하고 프로세스를 연결합니다.

 

3. 경고창 확인

경고창이 뜨면 Insecure connection을 누릅니다.

 

4. 원하는 탭에서 사용

여러가지 탭

메모리에서 현재 사용하는 메모리 사용량

Threads에서 동작중인 Thread리스트 등등 각각 원하는 탭에서 상태를 확인하시면 됩니다.

 

5. 저는 Thread가 정상적으로 동작하고 종료되는 모습을 확인하기 위해 Threads탭을 클릭합니다.

Thread개수와 사용중인 Thread리스트가 나오고 Thread를 클릭하면 상세정보가 나옵니다.

 

해당 소스에서는 shutdownGracefully(); 처리가 없는 상태로 nioEventLoopGroup이 사용할때마다 추가되고 있는 모습을 볼 수 있습니다.

 

6. shutdownGracefully();를 통해 Thread 종료 처리 후 확인하기

특정 행위가 만족되면서 new NettyClient에 의해 EventLoop가 생성으로 Threads에 항목이 추가되고, 추후 종료동작에서 Shutdown으로 인해 EventLoop에서 사라지는걸 볼 수 있습니다.

반응형
반응형

TCP 통신 해야하는 경우 JAVA SOCKET을 통해 서버를 구성하거나 클라이언트 소스를 구성하여 연결을 하고 메시지를 주고 받고 종료하고 데이터를 가공하거나 처리하거나 등등 여러가지를 할 수 있는데, 이를 좀 더 기능적으로 사용하기 쉽게 도와주는 네트워크 프레임워크로 Netty가 있습니다.

 

이번에 프로젝트를 하나 진행하면서 여러가지 요구사항을 보다 쉽게 수행하고 추후에 능률적으로 유지보수하기 위해 Netty 프레임워크를 통해 진행해본 과정을 튜토리얼 식으로 남겨볼까 합니다.

 

Spring 프로젝트에 서버와 클라이언트를 구성하여 필요한 경우 연동처리를 진행하였는데, 이번에는 Maven프로젝트에서 구성하도록 하겠습니다.

 

Netty Client

1. 먼저 메이븐 프로젝트를 생성합니다.

 

2. 생성 후 Netty와 Log처리를 위해 Maven추가를 합니다. (pom.xml)

<!-- Netty -->
<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.0.33.Final</version>
</dependency>

<!-- log4j -->
<dependency>
	<groupId>log4j</groupId>
	<artifactId>log4j</artifactId>
	<version>1.2.17</version>
</dependency>

pom.xml

 

3. 로그를 사용하기 위해 log4j 설정을 합니다.

# Root logger option
log4j.rootLogger=info, stdout, logfile

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p (%C{2}:%L) - %m%n

# Direct log message to log file
log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.DatePattern='.'yyyy-MM-dd
log4j.appender.logfile.File=C:/netty/logs/dailylog.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss}] %5p (%C{2} - %M:%L) - %m%n
log4j.appender.logfile.Append=true
log4j.appender.logfile.encoding=UTF-8

 

4. 옵션 설정과 동작시킬 client를 작성합니다 - NettyClient.java

package com.psw.socket.nettyPrj.netty.client;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import org.apache.log4j.Logger;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * NettyClient
 * @author psw
 */
public class NettyClient {
	private static final Logger logger = Logger.getLogger(NettyClient.class);
	
	private Bootstrap bs = new Bootstrap();
	private SocketAddress addr_;
	private String[] msgArr;
	
	public NettyClient(SocketAddress addr, String[] msgArr) {
		this.addr_ = addr;
		this.msgArr = msgArr;
	}
	
	public NettyClient(String host, int port, String[] msgArr) {
		this(new InetSocketAddress(host, port), msgArr);
	}
	
	//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
	public void run() {
		if(this.addr_ == null) {
			logger.error("주소 정보가 없습니다.");
		}else if(this.msgArr == null || this.msgArr.length == 0) {
			logger.error("보낼 메시지가 없습니다.");
		}
		
		bs.group(new NioEventLoopGroup(3))
		.channel(NioSocketChannel.class)
		.option(ChannelOption.SO_KEEPALIVE, true)
		.handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast("clientHandler", new NettyClientHandler(msgArr));
			}
		});
		
		ChannelFuture f = bs.connect(addr_);
		f.channel();
		logger.info(addr_ + " connect()");
	}
}

NettyClient 생성자를 통해 보낼 메시지 배열과 연결정보를 넘깁니다.

run()메소드를 통해 연결할 옵션을 설정하고 채널을 연결합니다.

 

5. 실질적으로 동작할 Handler를 작성합니다 - NettyClientHandler.java

package com.psw.socket.nettyPrj.netty.client;

import org.apache.log4j.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyClientHandler extends ChannelInboundHandlerAdapter{
	
	private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
	
	private String[] msgArr;
	private int idx = 0;
	public NettyClientHandler(String[] msgArr) {
		this.msgArr = msgArr;
	}
	
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널 등록");
	}
	
	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		System.out.println("채널 연결이 종료됨.");
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널이 메시지 발송할 준비가 됨.");
		sendMsg(ctx, this.idx);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		logger.info("메시지를 받는 메소드.");
		ByteBuf buf = (ByteBuf)msg;
		int n = buf.readableBytes();
		if( n > 0 ) {
			byte[] b = new byte[n];
			buf.readBytes(b);
			//수신메시지 출력
			String receiveMsg = new String( b );
			logger.info("수신된 메시지 >" + receiveMsg);
			
			//보낼 메시지가 없으면 연결 종료
			if(msgArr.length ==  ++this.idx) {
				channelClose(ctx);
			}else {
				sendMsg(ctx, this.idx);
			}
		}
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) {
		logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
	}
	
	private void sendMsg(ChannelHandlerContext ctx, int i) {
		String msg = msgArr[idx];
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(msg.getBytes());
		ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
		logger.info("발송 메시지 >" + msg);
	}
	
	private void channelClose(ChannelHandlerContext ctx) {
		logger.error("채널 연결 종료");
		ctx.close();
	}
}

handler는 ChannelInboundHandlerAdapter를 상속받아 사용하고 각 동작별 정의된 메소드를 통해 데이터를 전달하거나 받을 수 있습니다.

연결이 되자마자 전송할 메시지를 처리하고, read메소드에서는 서버에서 전송하는 메시지를 받습니다.

 

메소드별로 간단하게 정리를 해보겠습니다.

- channelRegistered : 채널이 등록되면 동작합니다. 등록만 되었지 입출력 이벤트는 동작하지 않습니다.

- channelUnregistered : 반대로 채널이 해제되는 경우 닫힌 경우 동작합니다.(본인이 종료하거나 서버측이 종료 됨.)

- channelActive : 채널에 입출력이 가능해지면 동작하는 메소드입니다.

- channelRead : 메시지가 수신되면 동작합니다.

- channelReadComplete : 메시지 수신이 끝나면 동작합니다. (더 이상 수신할 정보가 없을 경우)

- userEventTriggered : 유저가 커스텀하여 등록한 이벤트가 발생할 경우 동작합니다. (timeout 등)

- exceptionCaught : exception 발생

 

6. 동작 테스트 - App.java

package com.psw.socket.nettyPrj;

import org.apache.log4j.Logger;

import com.psw.socket.nettyPrj.netty.client.NettyClient;

public class App {
	private static final Logger logger = Logger.getLogger(App.class);
	
	public static void main( String[] args ){
		logger.info("======= Netty Client Test =======");
		String host = "127.0.0.1";
		int port = 15510;
		String[] msgArr = {"hello world\n", "hello world2\n", "hello world3\n"};
		
		
		new NettyClient(host, port, msgArr).run();
	}
}

저는 SockTest v3.0.0을 통해 테스트를 진행하였습니다.

 

클라이언트에서 메시지를 발송 테스트

hello world부터 hello world3까지 발송하는 모습을 볼 수 있습니다.

 

 

아래 URL에서 소켓테스트 프로그램을 받을 수 있습니다.

sourceforge.net/projects/sockettest/

 

SocketTest - Test My Socket

Download SocketTest - Test My Socket for free. SocketTest - powerful and small software tool for socket testing. It can create both TCP and UDP client or server.

sourceforge.net

 

반응형
반응형

Table 태그를 구성하다보면 데이터가 길어질 경우를 대비해서 스크롤을 내리더라도 헤더컬럼부분을 고정으로 주고

아래 데이터부분만 스크롤이 되도록 처리하는 경우가 있습니다.

헤더가 고정된 테이블

 

"<table>" 태그를 "<thead>"와 "<tbody>" 형태로 구성하시고 "<thead> <th>"에는 position: sticky를 처리하면 고정형 테이블을 구성할 수 있습니다.

 

다만, sticky의 경우 IE에서 지원이 안되기때문에 static으로 동작하여 일반적인 테이블처럼 동작하게 됩니다.

아래는 sticky를 사용하여 고정헤더 테이블 샘플 예제 소스입니다.

 

헤더 고정하기

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Table</title>
    <style>
        *{margin:0; padding:0;}
        html, body, .wrapper{width:100%; height: 100vh;}
        .wrapper .tableBox{position: relative; top: 50px; left: 50px; width: 700px; height: 100px; overflow-y: scroll; border-top: 2px solid dodgerblue;}
        .tableData{width: 100%; border-collapse: collapse; text-align: center;}
        .tableData thead tr th{position: sticky; top: 0; background: #ebeaea;}
        .tableData thead tr th,
        .tableData tbody tr td{padding: 15px 0; box-sizing: border-box; border-bottom: 1px solid #dedede; border-right: 1px solid #ddd; word-break: break-all;}
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="tableBox">
            <table class="tableData">
                <colgroup>
                    <col width="8%">
                    <col width="15%">
                    <col width="25%">
                    <col width="16%">
                    <col width="12%">
                    <col width="12%">
                    <col width="12%">
                </colgroup>
                <thead>
                    <tr>
                        <th class="stiky">순번</th>
                        <th class="stiky">구분</th>
                        <th class="stiky">제목</th>
                        <th class="stiky">글쓴이</th>
                        <th class="stiky">날짜</th>
                        <th class="stiky">첨부파일</th>
                        <th class="stiky">비고</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>공지사항</td>
                        <td>제목입니다.</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>공지사항</td>
                        <td>제목 "1","2","3","4","5","6","7","8","9","10","11"</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                    <tr>
                        <td>3</td>
                        <td>게시판</td>
                        <td>123456789123456789123456789</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

thead 태그의 th들에게 position:sticky를 처리하여 고정시킨 예제입니다.

주의점으로 theadthead tr에 sticky를 줘도 동작하지 않기에 꼭 thead영역의 th들에게 sticky처리를 하셔야 합니다.

 

 

myhappyman.tistory.com/185

 

HTML - Table td안에 특수문자(")가 존재할 경우 넓이 초과 현상

DB데이터를 가져와서 테이블에 데이터를 만들어 append를 처리하고 있었는데, 데이터 문자열 내부에 "(더블 쿼드)가 포함된 긴 문자열이 들어가 있었고, 정상적인 넓이로 table이 동작하지 않았다. '

myhappyman.tistory.com

이전 글인 table을 두개로 넓이를 동일하게 처리하여 구성한 테이블에서 발생할 수 있는 현상도 방지할 수 있고, 깔끔하게 동작이 됩니다!

 

IE를 신경쓰지 않아도 되는 프로젝트에서는 유용하게 사용할 수 있겠습니다.

반응형
반응형

DB데이터를 가져와서 테이블에 데이터를 만들어 append를 처리하고 있었는데, 데이터 문자열 내부에 "(더블 쿼드)가 포함된 긴 문자열이 들어가 있었고, 정상적인 넓이로 table이 동작하지 않았다.

 

'<colgroup>' 태그로 width를 주고 css로 width : 넓이값% !important까지 처리하였지만 여전히 먹통이였다.

word-break: break-all 속성을 추가하여도 문제가 발생하였는데,

원인은 특수문자 "(더블 쿼드)가  존재하면서 발생하는 문제였고, 파싱하는 과정에서 replace메소드로 치환하여 처리하니 문제없이 동작하였다.

 

아래는 비슷한 현상을 재현하기 위한 sample 소스이다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Table</title>
    <style>
        *{margin:0; padding:0;}
        html, body, .wrapper{width:100%; height: 100vh;}
        .wrapper .tableBox{position: relative; top: 50px; left:50px; width: 700px;}
        .wrapper .tableBox .scroll{width: 100%; height: 100px; overflow-y: scroll;}
        table{border-collapse:collapse;text-align: center;}
        table th{border-top: 2px solid dodgerblue; position: sticky; top: 0px;}
        table th, table td{border-bottom: 1px solid #dedede;border-right: 1px solid #ddd;word-break: break-all;}
        .tableHeader{width: calc(100% - 17px);}
        .tableHeader th{height: 40px;}
        .tableData{width: 100%;}
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="tableBox">
            <table class="tableHeader">
                <colgroup>
                    <col width="8%">
                    <col width="15%">
                    <col width="25%">
                    <col width="16%">
                    <col width="12%">
                    <col width="12%">
                    <col width="12%">
                </colgroup>
                <thead>
                    <tr>
                        <th>순번</th>
                        <th>구분</th>
                        <th>제목</th>
                        <th>글쓴이</th>
                        <th>날짜</th>
                        <th>첨부파일</th>
                        <th>비고</th>
                    </tr>
                </thead>
            </table>
            <div class="scroll">
                <table class="tableData">
                    <colgroup>
                        <col width="8%">
                        <col width="15%">
                        <col width="25%">
                        <col width="16%">
                        <col width="12%">
                        <col width="12%">
                        <col width="12%">
                    </colgroup>
                    <tbody>
                        <tr>
                            <td>1</td>
                            <td>공지사항</td>
                            <td>제목입니다.</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                        <tr>
                            <td>2</td>
                            <td>공지사항</td>
                            <td>제목 "1","2","3","4","5","6","7","8","9","10","11"</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                        <tr>
                            <td>3</td>
                            <td>게시판</td>
                            <td>123456789123456789123456789</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</body>
</html>

테이블 컬럼을 고정화 시키기 위해 table 태그를 2개로 구성하였다.

 

이후 로드된 페이지는 아래와 같다.

제목 컬럼의 내용이 특수문자가 추가되면서 주르륵 붙으면서 발생하는 현상이다.

 

더블쿼드가 붙으면 문제가 발생한다.

 

 

다음은 더블쿼드를 삭제하였을때 정상적으로 동작하는 것을 볼 수 있다.

 

더블쿼드를 지우니 깨지지 않는다.

 

 

반응형
반응형

스프링 프로젝트에서 Netty 프레임워크를 사용하여 'ServerBootstrap'을 구동하고 사용하였는데, 톰캣을 종료하면 자연스럽게 죽을줄 알았던 Netty 서버가 좀비처럼 계속 살아있는 현상이 발생했다.

 

덕분에 톰캣을 재기동하게 되면 같은 포트로 바인딩하면서 BindException이 발생하는 등 여러가지 문제가 생겼고, 대처법으로 netstat을 통해 port의 pid를 찾아와 kill을 시킬까 했지만, 애초에 종료를 안하는 원초적인 문제를 해결하는것이 맞다고 판단하여 아래와 같이 '@PreDestroy' 어노테이션을 추가하여, 스프링을 종료하게되면 ServerBootstrp을 종료 할 수 있도록 구성하였다.

 

Netty Server 안전하게 종료하기

Controller.java

private NettyServer server;

@PostConstruct
private void start() {
	new Thread(new Runnable() {
		
		@Override
		public void run() {
			try {
				server = new NettyServer(SERVER_PORT, service);
				server.run();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}).start();
}

/**
 * 바인딩된 NettyServer를 종료한다.
 * @throws InterruptedException
 */
@PreDestroy
private void destory() throws InterruptedException {
	logger.info("================ Spring @PreDestroy ================");
	server.serverDestroy();
}

 

NettyServer.java

private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ChannelFuture future;

public void serverDestroy() throws InterruptedException {
	logger.info("================ Netty BootStrapServer Destroy() ================");
	bossGroup.shutdownGracefully().sync();
	workerGroup.shutdownGracefully().sync();
	future.channel().closeFuture().sync();
}

구동하면서 동작시킨 이벤트 그룹들을 shutdown시켜서 종료되도록 처리하였다.

 

 

반응형
반응형

데이터를 검증하는 과정에서 문자열에 존재하는 데이터가 숫자만 있어야 하는 경우가 필요했는데, 이후 substring등으로 특정 형태로 변경해야해서 특히 검증을 꼼꼼하게 진행했습니다.

 

아래와 같이 정규식을 활용하면 쉽게 확인이 가능합니다.

public class StringUtils {

	public static void main(String[] args) {
		final String REGEX = "[0-9]+";
		String test = "20201119173455"; //년월일시분초
		
		if(test.matches(REGEX)) {
			System.out.println("숫자만 있습니다.");
		}else {
			System.out.println("숫자외에 값이 존재합니다.");
		}
	}
}

 

 

배열을 통해 여러가지 데이터를 검증해보았습니다.

public class StringUtils {

	public static void main(String[] args) {
		final String REGEX = "[0-9]+";
		String[] testArr = {"20201119173455", "20201119 173455", "2020111917:34:55"};
		for(int i=0; i<testArr.length; i++) {
			if(testArr[i].matches(REGEX)) {
				System.out.println("숫자만 있습니다.");
			}else {
				System.out.println("숫자외에 값이 존재합니다.");
			}
		}
	}
}

공백만 존재하여도 else를 타면서 정상적으로 체크하는 모습을 볼 수 있습니다.

반응형
반응형

다른곳에서 소스를 받아와서 복사 후 붙여넣기만 하면 VS CODE가 자동으로 줄 정렬을 해주는데, 이 기능이 굉장히 좋을 때도 있지만 불편하게 만드는 경우도 있다.

 

예를들면 css는 개인적으로 한줄로 쭉 작성하는것을 선호하는 편이지만 VS CODE에 넣기만 하면 자동으로 단위별로 줄 정렬이 되어버린다.

 

prettier와 같은 플러그인을 설치하여 옵션을 꺼봤지만 저장시 자동 정렬은 별도의 옵션이였다.

 

아래와 같이 옵션을 끄면 자동 정렬을 끌 수 있다.

 

저장 시 자동 줄 정렬 끄기🛠

 

1. 파일 - 기본 설정 - 설정

 

2. formatOnSave를 입력한다.

 

체크되어 있는 Format On Save 옵션을 체크를 해제하면 더 이상 저장할 때 자동으로 정렬이 되지 않는다.

반응형
반응형

파일 업로드 형태의 게시판 등 웹 UI에서 파일을 긁어와서 드롭하였을때 등록하고 ajax를 통해 업로드 하는 예제까지 진행해보겠습니다.(Spring Legacy 기준)

 

Drag And Drop

html, css

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<style>
    *{padding:0;margin:0}
    html, body, .wrap{width: 100%;}
    .clear{clear:both;}
    .wrap>.fileBox{padding: 20px;}
    .fileBox input, textarea{width: 100%;}
    .fileBox textarea{resize:none;}
    .fileBox .fileDrop{display: inline-block;width: 700px;height: 75px;border: 1px solid #000;overflow: auto;}
    .fileDrop .fileList .fileName{padding-left: 20px;}
    .fileDrop .fileList .fileSize{padding-right: 20px; float:right;}
</style>
<body>
    <div class="wrap">
        <div class="fileBox">
            <form id="fileForm" name="fileForm" enctype="multipart/form-data" method="post">
                <table>
                    <tr>
                        <td><input type="text" name="title"></td>
                    </tr>
                    <tr>
                        <td><textarea name="contents"></textarea></td>
                    </tr>
                    <tr>
                        <td><div id="fileDrop" class="fileDrop"></div></td>
                    </tr>
                </table>
                <div class="buttonBox">
                    <button type="button" id="save">저장</button>
                </div>
            </form>
        </div>
    </div>
</body>
</html>

js

var fileList = []; //파일 정보를 담아 둘 배열
$(function(){

    //드래그앤드랍
    $("#fileDrop").on("dragenter", function(e){
        e.preventDefault();
        e.stopPropagation();
    }).on("dragover", function(e){
        e.preventDefault();
        e.stopPropagation();
        $(this).css("background-color", "#FFD8D8");
    }).on("dragleave", function(e){
        e.preventDefault();
        e.stopPropagation();
        $(this).css("background-color", "#FFF");
    }).on("drop", function(e){
        e.preventDefault();

        var files = e.originalEvent.dataTransfer.files;
        if(files != null && files != undefined){
            var tag = "";
            for(i=0; i<files.length; i++){
                var f = files[i];
                fileList.push(f);
                var fileName = f.name;
                var fileSize = f.size / 1024 / 1024;
                fileSize = fileSize < 1 ? fileSize.toFixed(3) : fileSize.toFixed(1);
                tag += 
                        "<div class='fileList'>" +
                            "<span class='fileName'>"+fileName+"</span>" +
                            "<span class='fileSize'>"+fileSize+" MB</span>" +
                            "<span class='clear'></span>" +
                        "</div>";
            }
            $(this).append(tag);
        }

        $(this).css("background-color", "#FFF");
    });

    //저장
    $(document).on("click", "#save", function(){
        var formData = new FormData($("#fileForm")[0]);
        if(fileList.length > 0){
            fileList.forEach(function(f){
                formData.append("fileList", f);
            });
        }         

        $.ajax({
            url : "서버 맵핑 URL",
            data : formData,
            type:'POST',
            enctype:'multipart/form-data',
            processData:false,
            contentType:false,
            dataType:'json',
            cache:false,
            success:function(res){
                alert("저장에 성공하셨습니다.");
            },error:function(res){
                alert("오류 발생.\n관리자에게 문의해주세요.");
            }
        });
    });
});

drag관련 이벤트를 처리하고 싶은 개체에 등록하여 enter, over, leave, drop에 따른 처리를 각각 처리하였습니다.

drop하였을때는 파일을 내려놨을때 드래그앤드롭의 동작 중 가지고 있던 데이터의 정보를 확인하기 위해 dataTransfer 파일 정보를 가져옵니다.

 

이후 파일 정보가 담긴 object를 통해 하나씩 확인하여 tag를 생성하고 별도의 파일정보는 배열에 담아두었다가 서버에 전송시 사용합니다.

 

java - Server Controller

@ResponseBody
@RequestMapping(value = { "uploadPath" }, method = RequestMethod.POST, produces = "json/plain;charset=UTF-8")
public int uploadPath(MultipartHttpServletRequest mtfRequest, 
		final HttpServletRequest request, 
		final HttpServletResponse response) {
	int res = 1;
	System.out.println("제목 > " + request.getParameter("title"));
	System.out.println("내용 > " + request.getParameter("contents"));
	if(mtfRequest != null) {
		List<MultipartFile> fileList = mtfRequest.getFiles("fileList");
		for(int i=0; i<fileList.size(); i++) {
			MultipartFile multi = fileList.get(i);
			if(multi == null) {
				return 0;
			}else if(multi.getSize() == 0) {
				return 0;
			}else {
				System.out.println("파일명 : " + multi.getOriginalFilename() + " / 파일 사이즈 : " + multi.getSize());
			}
		}
	}
		
	return res;
}

ajax에서 전달한 데이터를 받을 컨트롤러입니다. 별도의 서비스나 처리는 하지 않았습니다.

전달된 데이터의 파라미터가 출력된 콘솔 결과는 아래와 같습니다.

컨트롤러에 전달된 데이터 정보

 

동작결과

반응형