앞서 부서별로 직원 수를 구하는 문제를

3가지 방법으로 살펴 보았다.

조인(Join) 4번, 하위쿼리(SubQuery)에서 1번과 4번 문제로

정리하는 의미에서

부서 이동 문제를 이용하여 다음과 같이 정리하였다.


실습 2 - GROUP의 6번 문제에서

[다음 그림과 같이 직원들의 부서 이동 과정을 출력하시오] 라는 문제를 풀었다.

SELECT DE.EMP_NO, GROUP_CONCAT(DE.DEPT_NO SEPARATOR ' > ' ) PATH
  FROM DEPT_EMP DE
GROUP BY DE.EMP_NO
HAVING COUNT(*)>1
ORDER BY COUNT(*) DESC;

이 문제는 좀 더 많은 설명과 구현이 필요한 SQL문이다.

먼저, 다음 그림과 같이 실행 결과에서 직원의 이름과 부서 명이 필요하다.

(아래의 설명을 읽는 것도 좋겠지만

위와 같이 출력되게 하는 SQL문을 직접 작성해 보면 실력향상에 도움이 될 것이다.)

SELECT DE.EMP_NO, E.FIRST_NAME, E.LAST_NAME, GROUP_CONCAT(D.DEPT_NAME SEPARATOR ' > ' ) PATH
  FROM DEPT_EMP DE
 INNER JOIN DEPARTMENTS D ON D.DEPT_NO=DE.DEPT_NO
 INNER JOIN EMPLOYEES E ON E.EMP_NO=DE.EMP_NO

GROUP BY DE.EMP_NO, E.FIRST_NAME, E.LAST_NAME
HAVING COUNT(*)>1
ORDER BY COUNT(*) DESC;

직원의 이름(FIRST_NAME, LAST_NAME)과

부서 명(DEPT_NAME)이 출력되기 위해서는

직원 테이블(EMPLOYEES)과 부서 테이블(DEPARTMENTS)을 추가해 주면 된다.

그리고, 직원 정보로 GROUP화하고

부서 명(DEPT_NAME)을 묶으면 된다.


여기서 생각해야 할 것이 GROUP으로 직원 정보를 사용하는 것이다.

직원명외에 생일을 추가한다고 할 경우

GROUP에 생일 필드를 추가해야 한다.

GROUP에 가변길이 문자열을 많은 추가하는 것은

SQL 성능(속도)상 추천되지 않는다.

다음과 같이 하위쿼리(SubQuery)를 이용하는 것이 바람직하다.

SELECT DS.EMP_NO, E.FIRST_NAME, E.LAST_NAME, PATH
  FROM (
        SELECT DE.EMP_NO, GROUP_CONCAT(D.DEPT_NAME SEPARATOR ' > ' ) PATH, COUNT(*) CNT
          FROM DEPT_EMP DE
          INNER JOIN DEPARTMENTS D ON D.DEPT_NO=DE.DEPT_NO
        GROUP BY DE.EMP_NO
        HAVING COUNT(*)>1
) DS
 INNER JOIN EMPLOYEES E ON E.EMP_NO=DS.EMP_NO
 ORDER BY CNT DESC;

부서는 조인을 이용하지만

개인정보는 하위쿼리를 사용하는 것이 좋다.

GROUP에 사용된 다는 것은 인덱스가 필요하다는 의미로

좀 더 복잡한 처리가 필요한 것도 있지만,

단순하게 데이터 개수의 의미도 있다.


조인을 사용할 경우

직원 수가 약 30만(300,024)이고,

부서 할당 정보가 약 33만 (331,603)이니

30만 * 33만의 처리가 발생한다.


하위 쿼리로 실행하면,

다음 SQL에서 알수 있듯이

처리 개수가 3만(31,579)개로 줄어든다.

부서 이동이 발생한 직원들만 처리하기 때문이며,

당연이 조인시 발생하는 처리 수도 줄어 든다.

SELECT COUNT(*)
  FROM (
        SELECT DE.EMP_NO, GROUP_CONCAT(D.DEPT_NAME SEPARATOR ' > ' ) PATH, COUNT(*) CNT
          FROM DEPT_EMP DE
          INNER JOIN DEPARTMENTS D ON D.DEPT_NO=DE.DEPT_NO
         GROUP BY DE.EMP_NO
         HAVING COUNT(*)>1
) DS


제대로 완성 시키려면,

현재 근무하는 직원만 출력해야 한다.

현재 근무하는 직원은

부서 할당(DEPT_EMP) 테이블에

근무 종료(TO_DATE)가 9999-01-01인 직원들이므로

다음과 같이 두개의 부서 할당 테이블이 필요하다.

하나는 현재 근무 중인 직원을 가져오는 테이블(DEPT_EMP DE1),

나머지는 부서 이동 정보를 가져오는 테이블(DEPT_EMP DE2)이다.

SELECT DS.EMP_NO, E.FIRST_NAME, E.LAST_NAME, PATH
  FROM (
        SELECT DE1.EMP_NO, GROUP_CONCAT(D.DEPT_NAME SEPARATOR ' > ') PATH, COUNT(*) CNT
          FROM DEPT_EMP DE1
         INNER JOIN DEPT_EMP DE2 ON DE2.EMP_NO=DE1.EMP_NO
         INNER JOIN DEPARTMENTS D ON D.DEPT_NO=DE2.DEPT_NO
         WHERE DE1.TO_DATE='9999-01-01'
         GROUP BY DE1.EMP_NO
        HAVING COUNT(*)>1
) DS
 INNER JOIN EMPLOYEES E ON E.EMP_NO=DS.EMP_NO
 ORDER BY CNT DESC;

부서 테이블과의 조인은(D.DEPT_NO=DE2.DEPT_NO)

부서 이동 정보를 가져오는 테이블(DEPT_EMP DE2)이다.



'Database > SQL 연습' 카테고리의 다른 글

실습 2 - GROUP  (2) 2016.04.02
실습 3 - Join  (0) 2016.04.02
실습 4 - SubQuery  (0) 2016.04.02
연습 1  (3) 2016.04.02
연습 2  (0) 2016.04.02

데이터 베이스를 기반으로 하는 프로그램들은

게시판만 잘 만들 줄 알아도 대부분의 개발은 되는 것으로 알고 있다.

모든 프로그램은 입력(INSERT), 수정(UPDATE), 삭제(DELETE), 조회(SELECT) 기능이 다양한 형태로 표현되는 것으로,

이러한 기능을 가장 간단하게 구현할 수 있는 것이 게시판 이라고 생각하기 때문이다.

따라서, 대단한 기술이 없이 단순 게시판만 잘 만들 줄 알아도 대부분의 프로그램을 문제 없이 만들 수 있다.

오히려 어설프게 아는 기술들을 적용해서 프로젝트를 망치는 경우를 매우 많이 봤다.

여기서는 게시판 만드는 과정을 단계별로 구성하여

보다 쉽고 깊이 있게 배울 수 있도록 시도하였다.

(게시판을 만들줄 알면 나머지는 프로세스에 따른 구현과 SQL 사용 능력인데 여기서는 다루지 않는다.)


본 게시판 예제는 Spring 4.1과 MyBatis 3.2를 기반으로 구성하였다.

Spring 4를 기반으로 제작했지만 

Spring 4의 특징들을 반영하지 않았기 때문에 Spring 3(전자정부 프레임)에서도 사용 가능하다.

다음 그림은 본 예제에서 사용한 Spring MVC 구조로 기본 구조보다 단순하게 구성하였다.   

Spring MVC에서는 DAO(mapper)와 Service interface(추상화)등의 구성이 더 구현되어야 한다.

하지만, 경험상 필요성을 느끼지 못하고 이러한 구조는 복잡성을 가중시킨다고 생각하여

본 예제에서는 다음 그림과 같이 단순화 하여 구성하였다.

즉, 추상화를 사용하지 않았고 DAO와 Servic를 합쳐서 Service로 사용하였다.


본 예제는 게시판을 개발하며 프로그래밍 하는 과정을 익히는 것으로

Spring과 MyBatis, MVC에 대하여 어는 정도 알고 있다는 전제로 진행되기 때문에

상세한 내용을 적지는 않을 것이지만 개발 구성에 대해서 간단하게 서술하였다.



사용자가 웹 브라우저를 통하여 특정 페이지에 접속하면 웹 서버(실제로는 WAS)는 해당 페이지의 컨트롤을 호출하고,

사용자가 입력하거나 처리에 필요한 정보를 구성하여 작성한 HTML을 웹 서버에 반환하게 되고

웹 서버는 해당 내용을 클라이언트(웹브라우저)에게 전송하면 사용자가 원하는 내용을 보게 된다.

다만 컨트롤이 처리할 때, 데이터 베이스 관련 작업이 필요한 경우 서비스를 호출하여 해당 데이터 처리와 관련된 실행(SQL)을 처리하여 반환하게 된다.




게시판의 리스트를 예로 들면,

사용자가 게시판 리스트(board1List)를 선택한 경우 웹 서버는 컨트롤인 board1List(Java)를 호출한다.

컨트롤인 board1List는 데이터 베이스에서 게시판 데이터를 가져오기 위해 서비스인 boardSvc를 호출하고

boardSvc는 Mybtis를 통해 데이터를 가져올 수 있는 SQL문을 board1.xml 파일에서 찾아서 실행시키게 된다. 

실행 결과로 게시판 데이터들을 집합(RecordSet)으로 받게 되고 이 데이터를 컨트롤에게 반환한다.


컨트롤은 이 데이터와 필요한 데이터를 묶어서 HTML로 구성된 board1List.jsp로 넘겨주고,

JSP 파일에서는 Taglib와 HTML로 사용자가 보는 화면을 구성하게 된다.

즉, MVC ((Model–View–Controller)로 운영되는 것이다.

데이터 베이스는 MariaDB를 대상으로 작성하였으나 MySQL에서도 사용가능하며,

Limit나 DATE_FORMAT등 함수 몇 가지를 수정하면 표준 SQL이라 다른 데이터 베이스에서도 사용 가능하다.



'Java > 게시판기본' 카테고리의 다른 글

2. 게시판의 구성  (4) 2016.04.02
3. 설치  (18) 2016.04.02
4. 게시판 구성 단계  (14) 2016.04.02

프로그래밍 언어에 관계없이 온라인 게시판은 일반적으로 다음 그림과 같이 구성된다.


  • 리스트: 게시판의 모든 내용을 웹 페이지에 출력 하는 것으로 게시판 테이블(TBL_BOARD)의 내용을 가져와서(Select) 하여 지정된 형태로 출력하게 된다.
  • 글 읽기: 리스트의 게시물 하나(한 행)를 선택하면 선택한 게시물에 대한 상세한 내용을 볼 수 있는 글 읽기 페이지로 이동한다. 게시물을 확인하고 리스트로 돌아가거나 글 수정이나 삭제를 할 수 있다.
  • 글 쓰기: 리스트 페이지에서 [글쓰기] 링크를 선택하면 나타나는 페이지로 사용자가 입력할 화면이 나오고, 입력 후 [저장] 버튼을 클릭하면 테이블에 저장(Insert) 한 후 리스트로 돌아가서 저장된 내용을 볼 수 있게 한다.
  • 글 수정: 글쓰기와 같은 화면으로 구성되고, 글 읽기에서 [글수정]을 선택하면 이동한다. 수정할 내용을 입력 후 저장] 버튼을 클릭하면 테이블에 저장(Update)한 후 리스트로 돌아가서 저장된 내용을 볼 수 있게 한다.
  • 글 삭제: 글 읽기에서 [글삭제]를 선택하면 화면 없이 삭제를 진행하고 리스트로 돌아가서 삭제된 내용을 볼 수 있게 한다.

글쓰기를 제외하고는 기본키(Primary Key, PK) 값이 있어야 해당 글을 읽거나 수정/ 삭제 할 수 있다.

게시판에서는 기본키가 글 번호가 된다.

개발하는 방식에 따라 글 수정과 삭제 링크를 글읽기가 아닌 리스트에 둘 수 있지만 본 예제에서는 위 그림처럼 진행한다.





이 그림은 게시판의 구성에 컨트롤명(웹페이지명)을 매핑한 그림이다.

사용자는 게시판 리스트 페이지에 접속하고 웹서버는 리스트 페이지를 제공하기 위해 boardList 컨트롤을 호출한다.

하나의 Java 파일(board1Ctr, board2Ctr 등)에 모아져 있으니 먼저 확인하면 좋을 것이다.

또, 이 컨트롤에서 모든 처리가 이루어지기 때문에 먼저 설명한다.


이하의 게시판 예제는 GitHub에서 다운 받을 수 있다.

'Java > 게시판기본' 카테고리의 다른 글

Spring 4 + MyBatis 3 + MariaDB (Maven) 기반 게시판 예제  (3) 2016.04.02
3. 설치  (18) 2016.04.02
4. 게시판 구성 단계  (14) 2016.04.02

여기서는 다루지 않지만 기본적으로 다음 사항은 설치가 되어 있어야 한다.

설치 되지 않았다면 참고할 사이트를 작성하였으니 읽고 설치 해야 한다.


이클립스(Eclipse)에서 가져오기(import)나 새로만들기(New)를 실행하여

다음 그림과 같이 Git(Projects from Git)을 선택한다.

다음 화면에서 Clone URI를 선택한다.

Github의 게시판 예제 사이트 (https://github.com/gujc71/board_sample)에서

본 샘플 Git URL(https://github.com/gujc71/board_sample.git)을 복사한다.

이클립스에서 URL에 붙여넣기를 한다.

가져온 샘플을 저장할 디렉토리를 지정한다.

디렉토리는 이클립스에서 지정된 workspace 하위에 있어야 한다.

샘플로 생성할 프로젝트 종류를 지정하는 화면으로

다음과 같이 "Import existing Eclipse projects"를 선택한다.

다음 화면에서 완료(finish)를 선택하면

Github에서 데이터를 가져와 컴파일하는 화면이 나타난다.

Maven을 사용했기 때문에

다음 그림과 같은 프로젝트가 생성되고,

Maven이 필요한 라이브러리(jar)를 설치해 준다.

위 트리에 빨간색 X 표시 아이콘이 나타날 경우가 있다.

해당 파일을 열어 보면 자바 기본 클래스들이 모두 오류 표시가 된 경우는 자바 버전이 맞지 않은 것이다.

제공되는 소스는 자바 1.7로 개발되었는데 설치한 사람의 PC에는 다른 버전의 자바가 설치 된 경우 이다.


위 트리의 상위(프로젝트)를 선택하고 마우스 오른 쪽을 눌러 "Properties"를 실행하고

Java Build Path > Libaraies 탭을 선택한다.

JRE System Library에 jre 버전을 확인하고 Edit 버튼으로 설치된 자바 버전으로 변경한다.

board\src\main\webapp\WEB-INF하위에 있는

applicationContext.xml파일을 열어서 MariaDB의 적절한 url와 아이디/비밀번호를 넣고

톰캣을 실행한다.

MariaDB에는 다음 테이블이 생성되어 있어야 한다.

CREATE TABLE TBL_BOARD (
  BRDNO int(11) NOT NULL AUTO_INCREMENT,
  BRDTITLE varchar(255),
  BRDWRITER varchar(20),
  BRDMEMO   varchar(4000),
  BRDDATE   datetime,
  BRDHIT INT,
  BRDDELETEFLAG CHAR(1),
  PRIMARY KEY (BRDNO)
) ;

CREATE TABLE TBL_BOARDFILE (
    FILENO INT(11)  NOT NULL AUTO_INCREMENT,
    BRDNO INT(11),
    FILENAME VARCHAR(100),
    REALNAME VARCHAR(30),
    FILESIZE INT,
    PRIMARY KEY (FILENO)
);


웹브라우저에서 http://localhost:8080/board/board1List를 입력하여

실행해 보면 다음 그림과 같이 게시판이 실행된다.


게시판 예제는 다음 그림과 같은 구조로 제작되었다.

Java > gu 폴더에 서비스, 컨트롤 등의 Java 파일이 있고

Resources > sql 폴더에 SQL 문이 Mybatis의 XML 파일로 있다.

Webapp > WEB-INF > jsp 폴더에 JSP(HTML)로 작성된 파일이 있다.

gu 폴더 하위의 board1 폴더,

sql 폴더에 있는 board1.xml, 

jsp 폴더 하위의 board1 폴더가 각 단계별 하나의 묶음으로 구성되었다.


gu 폴더 하위의 board1 폴더 하위에 있는

board1Ctr.java에 기본 게시판에 사용된 모든 컨트롤

board1Src.java에 기본 게시판에 사용된 모든 서비스가 있다.

gu 폴더 하위의 board2, 3 등의  폴더 하위에도 동일한 규칙이 적용되었다.

board1Ctr, board2Ctr등과 board1Src, board2Src 등이 아닌

boardCtr 과 boardSrc로 각 폴더별로 동일한 명칭을 사용하면 좀더 편한데

이러한 명명 규칙은 Java에서는 허용하지만 Spring에서 허용되지 않아서 앞서의 명명 규칙처럼 다른 이름을 사용하였다.





'Java > 게시판기본' 카테고리의 다른 글

Spring 4 + MyBatis 3 + MariaDB (Maven) 기반 게시판 예제  (3) 2016.04.02
2. 게시판의 구성  (4) 2016.04.02
4. 게시판 구성 단계  (14) 2016.04.02

본 샘플은 Spring 4 + MyBatis 3 + MariaDB (Maven) 기반으로 게시판을 만드는 과정을 단계별로 구현한 샘플이다.

각 내용은 다음과 같이 구성되었다.


1. board Step 1 (게시판1)
    List: 모든 게시물 출력
    Form: 사용자 입력 내용 저장
    Update: 사용자 입력 내용 수정
    Read: 사용자 입력 내용 보기
    Delete: 지정된 게시물 삭제

    URL: http://localhost:8080/board/board1List


2. board Step 2 (게시판2)

    List: 페이징, 새로운 번호 부여
    Form: 입력/수정을 하나로
    Read: 조회수
    Delete: 삭제에서 숨기기로

    URL: http://localhost:8080/board/board2List


3. board Step 3 (게시판3)

    List: 검색, 제목을 한 줄로 표시 ==> 페이징을 공통으로
    Form: 필수입력, 수정/저장 서비스 하나로
    Read: 스크립트 실행 방지

4. board Step 4 (게시판4)

    자료실

5. board Step 5 (게시판5)

    댓글

6. board Step 6 (게시판6)

    무한 댓글 (계층형)

7. board Step 7 (게시판7)

    JQuery / Ajax

8. board Step 8
(게시판8)

    멀티 게시판

9. board Step 9 - (게시판9)

    멀티 게시판 관리자


'Java > 게시판기본' 카테고리의 다른 글

Spring 4 + MyBatis 3 + MariaDB (Maven) 기반 게시판 예제  (3) 2016.04.02
2. 게시판의 구성  (4) 2016.04.02
3. 설치  (18) 2016.04.02

파일을 첨부하기 위해서는 글 쓰기 폼(boardForm.jsp)에 file 태그만 추가해 주면 된다.

다음 코드에서 빨갛게 표시된 것처럼 uploadfile파일이라고 이름을 주고 생성하였다. 

Multiple 속성은 한 번에 여러 개의 파일을 지정할 수 있다.

<input type="file" name="uploadfile" multiple="" />

이러한 첨부 파일을 전송하기 위해 폼 태그도 인코딩 타입(enctype)을 multipart/form-data로 지정해 줘야 한다. 

이것은 파일이나 용량이 큰 데이터를 전송할 때 지정하는 방식으로 기억해두면 될 것 같다.

<form name="form1" action="board4Save" method="post" enctype="multipart/form-data">
    <table border="1" style="width:600px">
        ~~ 생략 ~~
            <tr>
                <td>내용</td>

                <td><textarea name="brdmemo" rows="5" cols="60"><c:out value="${boardInfo.brdmemo}"/></textarea></td>
            </tr>
            <tr>
                <td>첨부</td>
                <td>
                    <input type="file" name="uploadfile" multiple="" />

boardForm.jsp

사용자가 파일을 첨부하여 전송한 내용을 서버에서 받기 위해 스프링에서 제공하는 MultipartFile를 다음 코드와 같이 boardVO에 추가해 줘야 한다.

다수의 파일을 전송하기 때문에 List 형태로 지정해야 하고 변수 이름(uploadfile)은 파일 태그에서 지정한 이름과 같아야 한다.

public class boardVO {

    private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag;
    private List<MultipartFile> uploadfile;

    ~~ 생략 ~~
    public List<MultipartFile> getUploadfile() {
        return uploadfile;
    }


    public void setUploadfile(List<MultipartFile> uploadfile) {
        this.uploadfile = uploadfile;
    }

boardVO.java

파일을 저장하기 위해서 관련 내용을 boardVO로 받아온 뒤에 처리를 하게 된다.

먼저, MultipartFile로 받아온 파일들을 서버의 지정된 디렉터리에 저장해야 한다.

본 예제에서는 FileUtil라는 클래스로 이 기능을 미리 개발해 두었다. 

복잡하지 않기 때문에 원리를 이해하면 좋겠지만 여기에서는 saveAllFiles만 호출하면 된다는 것을 기억해 두자. 

다만 FileUtil 안에 저장 디렉터리로 "d:\\workspace\\fileupload\\"가 지정되어 있으니 각자의 디렉터리 구조에 맞추어 수정해줘야 하다.

@RequestMapping(value = "/board4Save")
public String boardSave(HttpServletRequest request, boardVO boardInfo) throws Exception {
    FileUtil fs = new FileUtil();

    List<FileVO> filelist = fs.saveAllFiles(boardInfo.getUploadfile());

    boardSvc.insertBoard(boardInfo, filelist);

    return "redirect:/board4List";
}

board4Ctr.java

주의: 파일명은 날짜와 시간(millisecond)을 이용하여 중복되지 않게 부여하고, 각 파일은 해당 년도 디렉터리에 저장하게 된다.

디렉터리는 파일명의 앞 4자리(년도)를 잘라서 자동 생성되고 그 디렉터리에 파일이 저장된다.

하나의 디렉터리에 저장될 수 있는 파일의 개수는 한정적(약 만개로 기억)이라 년도 별로 저장하게 개발하였다.

만약, 1년간 파일의 개수가 제한된 개수를 넘는다면 년월(앞6자리)로 수정해 주면 된다.


첨부된 파일이 실제 디렉터리에 저장되면 FileVO 형의 List가 반환된다.

FileVO는 파일 첨부 기능이 있는 모든 페이지에서 비슷하게 사용하기 때문에 공통(common)으로 제작하였다.

클래스 구성은 테이블(TBL_BOARDFILE) 구조와 유사하지만

테이블의 글번호(brdno) 대신에 부모글 번호(parentPK)로 명명하여 공통으로 사용할 수 있게 구성하였다.

size2String 함수는 비트로 저장된 파일 크기(filesize)를 byte, K-byte, M-Byte 등으로

적절하게 변환하여 반환하는 함수로 공통 함수로 구성하여 사용해도 되지만

FileVO 외에서는 사용할 일이 없어서 FileVO에 넣어서 제작하였다.

사용된 공식은 몇 년 전 인터넷에서 찾은 것으로 간단해서 익혀두면 조금이나마 알고리즘 연습이 될 수 있을 것이다.

package gu.common;

public class FileVO {
    private Integer fileno;         // 글번호
    private String parentPK;     // 부모 글번호
    private String filename;     // 파일명
    private String realname;    // 실제파일명
    private long filesize;        // 파일 크기
   
    public String size2String() {
        Integer unit = 1024;
        if (filesize < unit){
            return String.format("(%d B)", filesize);
        }
        int exp = (int) (Math.log(filesize) / Math.log(unit));

        return String.format("(%.0f %s)", filesize / Math.pow(unit, exp), "KMGTPE".charAt(exp-1));
    }

   
    public Integer getFileno() {
        return fileno;
    }

    public void setFileno(Integer fileno) {
        this.fileno = fileno;
    }
    ~~ 생략 ~~

FileVO.java

원리는 로그(log)를 이용한 것으로 밑수가 10인 로그(상용로그)를 실행하면 10의 배수가 계산된다.

byte, K-byte, M-Byte 등은 1024배씩 커지는 것으로 약 10의 3배수씩 커진다고 보면 된다.

즉, 1 K byte = 1024 byte 이고 1024 byte 는 Log를 취하면 약 3(3.010)의 값이 나온다.

이 3은 10의 자릿수로 보면 된다 (1000으로 보면 0의 개수).

예로, 파일 크기(filesize)가 2248 byte일 경우 로그를 취하면 약 3(3.351)이 나오고

단위 (1024의 로그값 3)로 나누면 1(1.11)이 계산 된다.

즉, 단위를 나타내는 문자열("KMGTPE")에서 첫 번째 K가 선택되게 된다.

단위를 구했으면 단위에 맞게끔 파일 크기를 계산해 줘야 한다.

단위값(1024)을 거듭제곱(power)으로 1을 계산하면 1024가 나오고 이 값을 실제 파일 크기 2248에 대하여 나누면 2.19가 계산된다.

따라서 최종적으로 2.2 K byte로 표기되는 것이다.

이렇게 반환된 파일 리스트(filelist)는 게시물 내용과 같이 서비스(insertBoard)로 전달하여 데이터 베이스에 저장한다.

public void insertBoard(boardVO param, List<FileVO> filelist) throws Exception {
        if (param.getBrdno()==null || "".equals(param.getBrdno()))
               sqlSession.insert("insertBoard4", param);
        else sqlSession.update("updateBoard4", param);

        for (FileVO f : filelist) {
            f.setParentPK(param.getBrdno());
           sqlSession.insert("insertBoard4File", f);
        }
}

board4Svc.java

서비스에서는 파일 리스트 개수만큼 반복해서 다음과 같이 Insert문으로 첨부

테이블(TBL_BOARDFILE)에 저장하면 된다.

게시물의 글번호(brdno)를 parentPK에 넣고(setParentPK), mybatis에서는 INSERT문을 #{parentPK}로 만들어 실행하게 된다. 

<insert id="insertBoard4File" parameterType="gu.common.FileVO" >
        INSERT INTO TBL_BOARDFILE (BRDNO, FILENAME, REALNAME, FILESIZE)
        VALUES (#{parentPK}, #{filename}, #{realname}, #{filesize})
</insert>

board4.xml

게시판 테이블(TBL_BOARD)에 저장하는 insertBoard4의 SQL문도 수정이 필요하다.
위 SQL의 insertBoard4File에 게시판의 글번호(brdno) 값이 필요하다.
글 수정일 경우 글번호가 같이 넘어 오지만
신규 글 쓰기일 경우 글번호가 부여 되지 않았다.
특히, 글 쓰기는 글번호가 Insert문이 실행되면 DBMS가 자동으로 부여하기 때문에 그 번호를 알 수 없다.
따라서, 자동으로 부여하지 않고 계산해서 가져와야 한다.
자동으로 부여 하는 방법은 현재 테이블에 저장된 번호 중 최대값(MAX)을 구하고, 그 값에 1을 더하면 된다.


            SELECT IFNULL(MAX(BRDNO),0)+1 FROM TBL_BOARD


IFNULL은 null일 때 0값을 주는 것으로 MAX 함수가 null 일 경우는 데이터가 하나도 없는 경우를 의미한다.
즉, 첫 데이터를 입력하기 전이다.
이렇게 반환 된 값으로 게시판 글번호로 저장하고 첨부 파일에도 저장하면 된다.
따라서 insertBoard4 서비스에 위 SELECT문을 실행할 수 있도록 하면 되지만,
Mybatis의 selectKey를 이용하면 쉽게 해결 할 수 있다.
다음 코드와 같이 selectKey의 keyProperty에 글번호(brdno)를 지정하면 신규 글번호 값이 boardVO 클래스의 brdno에 넣어져 반환 된다.
이 값이 INSERT문에서 #{brdno}로 사용해주면 새로운 게시물이 잘 저장된다.
그리고, 파라메타로 넘어온 boardVO 클래스에 저장되어 반환되기 때문에,
f.setParentPK(param.getBrdno())로 글번호를 받아와서 사용할 수 있게 된다.

<insert id="insertBoard4" parameterType="gu.board4.boardVO" >
        <selectKey resultType="String" keyProperty="brdno" order="BEFORE">
            SELECT IFNULL(MAX(BRDNO),0)+1 FROM TBL_BOARD
       </selectKey>
  
       INSERT INTO TBL_BOARD(BRDNO, BRDTITLE, BRDWRITER, BRDMEMO, BRDDATE, BRDHIT, BRDDELETEFLAG)
                              VALUES (#{brdno}, #{brdtitle}, #{brdwriter}, #{brdmemo}, NOW(), 0, 'N' )
</insert>

board4.xml

마지막으로 다음과 같이 트랜잭션(Transaction) 처리를 해주어야 한다.

앞서 insertBoard 서비스(board4Svc)에서 기존에 게시물 저장 후에

첨부한 파일 리스트들을 저장하는 SQL문을 실행하도록 추가했다.

즉 2개의 트랜잭션이 발생하는 경우에,

앞의 게시물을 저장하고 여러 가지 원인으로 오류가 발생 할 경우 첨부 파일 정보는 저장되지 않는다.

데이터 무결성에 문제가 발생한다.

이러한 경우 모든 데이터가 저장 안되게 처리하게 되는데 

다음과 같이 try 문을 사용하여 오류가 생기면 앞서 작업한 내용을 모두 취소(rollback)하고

오류가 없으면 모두 저장(commit)하도록 작성한다.

txManager 클래스는 이미 applicationContext.xml에 선언되어 있는 것으로 그냥 트랜잭션에 사용되는 것으로 기억하고 넘어간다.

DefaultTransactionDefinition ~~클래스 등은 트랜잭션 사용시 나오는 상용 구문 정도로 기억하면 될 듯하다.

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);

try{
    if (param.getBrdno()==null || "".equals(param.getBrdno()))
           sqlSession.insert("insertBoard4", param);
    else sqlSession.update("updateBoard4", param);

    for (FileVO f : filelist) {
        f.setParentPK(param.getBrdno());
        sqlSession.insert("insertBoard4File", f);
    }
    txManager.commit(status);
} catch (Exception ex) {

    txManager.rollback(status);
    throw ex;
}     

board4Svc.java

본 예제는 첨부한 파일을 "d:\workspace\fileupload\"에 저장한다.

지정된 디렉터리에 대한 운영체제의 권한에 따라 권한오류(특히 리눅스)가 발생하면서 파일이 저장 되지 않으니 주의해야 한다.




'Java > 게시판 4:자료실' 카테고리의 다른 글

1. 자료실 - 준비 & 시작  (1) 2016.04.02
3. 자료실 - 글 수정  (3) 2016.03.28
4. 자료실 - 리스트  (0) 2016.03.28
5. 자료실 - 글 읽기 / 삭제  (0) 2016.03.26

자료실에서 글 수정의 핵심은 다음 그림과 같이 기존에 첨부한 파일을 삭제할 수 있게 하는 것이다.



새로 첨부하는 것은 글쓰기와 수정이 하나로 처리되어 있기 때문에 별도의 처리가 필요치 않다.

먼저, 게시물과 같이 첨부 파일 리스트를 가져오도록 구성해 줘야 한다.

@RequestMapping(value = "/board4Form")
public String boardForm(HttpServletRequest request, ModelMap modelMap) throws Exception {
    String brdno = request.getParameter("brdno");
    if (brdno!=null) {
        boardVO boardInfo = boardSvc.selectBoardOne(brdno);
        List<?> listview = boardSvc.selectBoard4FileList(brdno);
   
        modelMap.addAttribute("boardInfo", boardInfo);
        modelMap.addAttribute("listview", listview);
    }
   
    return "board4/boardForm";
}

board4Ctr.java

다음 SELECT문은 첨부 테이블(TBL_BOARDFILE)에서 게시글에 해당하는 데이터를 가져오는 것으로,

서비스에 selectBoard4FileList를 추가해 주고,

컨트롤에서는 이 서비스를 호출하여 넘겨받은 List를 게시물 정보(boardInfo)와 같이 modelMap에 넣어(listview) jsp로 넘겨준다.

<select id="selectBoard4FileList" resultType="gu.common.FileVO" parameterType="String">
        SELECT FILENO, FILENAME, REALNAME, FILESIZE
          FROM TBL_BOARDFILE
         WHERE BRDNO=#{brdno}
         ORDER BY FILENO DESC
    </select>

baord4.xml

글 수정(쓰기) 폼에서는 listview의 개수만큼 반복(foreach)하여 파일 명을 보여주고 사용자가 선택하면 삭제 할 수 있도록 한다.

즉, 파일명을 보여 줄 때 사용자가 삭제하고자 선택 할 수 있는 checkbox를 주고

이름은 fileno로 동일하게 부여 하고 값(value)은 각 첨부 파일의 파일 번호(fileno)를 주도록 한다.

<form name="form1" action="board4Save" method="post" enctype="multipart/form-data">
    <table border="1" style="width:600px">
        ~~ 생략 ~~
            <tr>
                <td>내용</td>
                <td><textarea name="brdmemo" rows="5" cols="60"><c:out value="${boardInfo.brdmemo}"/></textarea></td>
            </tr>
            <tr>
                <td>첨부</td>
                <td>
                    <c:forEach var="listview" items="${listview}" varStatus="status">
                        <input type="checkbox" name="fileno" value="<c:out value="${listview.fileno}"/>">   
                        <a href="fileDownload?filename=<c:out value="${listview.filename}"/>&downname=<c:out value="${listview.realname}"/>">
                        <c:out value="${listview.filename}"/></a> <c:out value="${listview.size2String()}"/><br/>
                    </c:forEach>                   
               
                    <input type="file" name="uploadfile" multiple="" />

boardForm.jsp

예로 파일 이름과 번호(fileno)가 1, 2, 3인 경우 각각의 체크 박스가 동일한 이름으로 선택할 수 있게 생성하는 것이다. 

사용자가 1, 3 번을 선택하면 서버로 이 값이 배열로 fileno에 저장되어 전송된다.


파일명을 선택하면 해당 파일이 다운로드(fileDownload) 되도록 제작하였다.

fileDownload 컨트롤은 해당 파일명을 파라메타로 넘겨주면 지정된 파일을 찾아서 클라이언트로 전송해 준다.

fileDownload도 공통으로 사용하는 것으로 원리는 넘어가니 사용법만 기억해 두자.

파라메터 filename은 사용자가 입력한 파일명(filename)이고, downname은 서버에서 저장한 실제 파일명(realname)을 지정해 준다.

fileDownload에도 파일 경로가 고정되어 있으니 자신에게 맞게끔 수정하면 된다.


사용자가 삭제하기 위해 선택한 파일번호들을

다음과 같이 getParameterValues를 이용하여 배열로 받아와서 처리한다.

@RequestMapping(value = "/board4Save")
public String boardSave(HttpServletRequest request, boardVO boardInfo) throws Exception {
    String[] fileno = request.getParameterValues("fileno");

    FileUtil fs = new FileUtil();
    List<FileVO> filelist = fs.saveAllFiles(boardInfo.getUploadfile());

    boardSvc.insertBoard(boardInfo, filelist, fileno);

    return "redirect:/board4List";
}

board4Ctr.java


컨드롤에서 받은 파일번호들을(fileno) Delete문을 이용하여 삭제하면 개발이 끝나게 된다.

배열 변수 하나을 Mybatis로 넘길 수 없기 때문에,

배열변수를 가지는 클래스를 생성하거나 HashMap 클래스를 이용해야 한다.

본 예제에서는 후자를 이용했다.

public void insertBoard(boardVO param, List<FileVO> filelist, String[] fileno) throws Exception {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = txManager.getTransaction(def);
        
        try{
            if (param.getBrdno()==null || "".equals(param.getBrdno()))
                 sqlSession.insert("insertBoard4", param);
            else sqlSession.update("updateBoard4", param);
   
            if (fileno != null) {
                HashMap p = new HashMap();
                p.put("fileno", fileno) ;
                sqlSession.insert("deleteBoard4File", p);
            }

            for (FileVO f : filelist) {
                f.setParentPK(param.getBrdno());
                    sqlSession.insert("insertBoard4File", f);
            }
            txManager.commit(status);
        } catch (Exception ex) {
            txManager.rollback(status);
            throw ex;
        }            
}

board4Svc.java

Mybatis에서 넘겨 받은 삭제할 파일 번호들을 foreach문을 사용하여 삭제 하게 된다.

넘겨 받은 파일번호(fileno)의 배열 값을 콤마(,)로 생성하고

SQL문의 저장되어 있기 때문에 IN을 사용하여 한번에 지정된 모든 파일 정보를 삭제한다.

<delete id="deleteBoard4File" parameterType="hashmap">
    DELETE
      FROM TBL_BOARDFILE
         WHERE FILENO IN (
              <foreach item="item" index="index" collection="fileno" separator=",">
                     ${item}
              </foreach> 
        )    
</delete>

baord4.xml

삭제할 파일이 1번과 3번 파일이면 deleteBoard4File은 다음과 같은 SQL 문을 실행하게 될 것이다.

DELETE FROM TBL_BOARDFILE WHERE FILENO IN (1, 3)

클래스나 HashMap에 넘겨서 처리하는 방법외에

하나의 DELETE 문을 배열의 개수 만큼 반복 실행하는 방법도 있지만

개인적으로 데이터 입출력 횟수가 적은 것을 선호해서 이러한 방법을 사용했다.


앞서서 게시물 삭제를 실제로 삭제하지 않고 플래그(deleteflag) 처리하였는데 첨부 파일도 그렇게 구현할 수 있다. 

더욱이 실제 파일을 삭제하는 코드는 구현하지 않았는데 컨트롤에서 java delete 함수를 이용해 파일 삭제도 해보길 …

'Java > 게시판 4:자료실' 카테고리의 다른 글

1. 자료실 - 준비 & 시작  (1) 2016.04.02
2. 자료실 - 글쓰기  (0) 2016.03.28
4. 자료실 - 리스트  (0) 2016.03.28
5. 자료실 - 글 읽기 / 삭제  (0) 2016.03.26

리스트에서는 첨부 파일이 있는지 여부를 적절한 아이콘으로 보여준다.

리스트에서 다운로드 받게끔 하는 경우도 있지만 본 예제에서는 첨부 개수를 보여주어 첨부 파일 존재 여부만 표시 했다.


먼저, SQL에서 다음과 같이 subquery를 이용하여 첨부 파일 개수를 세어준다.

해당 게시물의 글번호(TB.BRDNO)에 맞는 첨부 파일의 게시물 번호(BRDNO)를 맞는 개수를 세는 것이다.

<select id="selectBoard4List" resultType="gu.board4.boardVO" parameterType="gu.common.SearchVO">
        SELECT BRDNO, BRDTITLE, BRDWRITER, DATE_FORMAT(BRDDATE,'%Y-%m-%d') BRDDATE, BRDHIT
                  , (SELECT COUNT(*) FROM TBL_BOARDFILE WHERE BRDNO=TB.BRDNO) FILECNT
          FROM TBL_BOARD TB
         <include refid="includeBoard"/>
         ORDER BY BRDNO DESC
         LIMIT ${rowStart-1}, 10
    </select>

baord4.xml

subquery의 필드명을 filecnt로 지정하고 boardVO에 추가해 준다.

public class boardVO {

    private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag
                     , filecnt;
    ~~ 생략 ~~
    public String getFilecnt() {
        return filecnt;
    }

    public void setFilecnt(String filecnt) {
        this.filecnt = filecnt;
    }

boardVO.java

마지막으로 jsp 파일에서 첨부 파일 개수(filecnt)를 보여주는 열을 다음과 추가하고 실행하여 결과를 확인하면 된다.

        <thead>
            <tr>
                <th>번호</th>
                <th>제목</th>
                <th>등록자</th>
                <th>등록일</th>
                <th>조회수</th>
                <th>첨부</th>
            </tr>
        </thead>
        <tbody>
            <c:forEach var="listview" items="${listview}" varStatus="status">   
                <c:url var="link" value="board4Read">
                    <c:param name="brdno" value="${listview.brdno}" />
                </c:url>       
                <tr>
                    <td><c:out value="${searchVO.totRow-((searchVO.page-1)*searchVO.displayRowCount + status.index)}"/></td>
                    <td><a href="${link}"><c:out value="${listview.getShortTitle(35)}"/></a></td>
                    <td><c:out value="${listview.brdwriter}"/></td>
                    <td><c:out value="${listview.brddate}"/></td>
                    <td><c:out value="${listview.brdhit}"/></td>
                    <td><c:out value="${listview.filecnt}"/></td>
                </tr>
            </c:forEach>
        </tbody>

boardList.jsp



'Java > 게시판 4:자료실' 카테고리의 다른 글

1. 자료실 - 준비 & 시작  (1) 2016.04.02
2. 자료실 - 글쓰기  (0) 2016.03.28
3. 자료실 - 글 수정  (3) 2016.03.28
5. 자료실 - 글 읽기 / 삭제  (0) 2016.03.26

게시판을 운영하다 보면,

사용자가 글 내용을 입력할 때 텍스트박스(TextArea)로 입력하는 게 아니라 웹 에디터를 이용할 때가 많다.

즉, HTML 태그를 사용자가 입력하는 것이다.

하지만 현재의 코드에서는 사용자가 입력한 HTML 코드가 실행되지 않는다. 

글쓰기에서 글 내용에 “<B>테스트</B>”라고 입력한 후 저장해 보자.

글 읽기에서 확인해보면 입력한 내용이 그대로 출력되는 것을 알 수 있다.



사용자가 원하는 것은 굵게 표시된 글자일 것이다.

이렇게 출력되는 것은 Spring에서 HTML 실행을 막아놨기 때문이다.

다음과 같이 출력(out) 테그에 escapeXml="false"를 넣어주면 문제가 해결된다.

<tr>
    <td>내용</td>
    <td><c:out value="${boardInfo.brdmemo}" escapeXml="false"/></td>
</tr>

boardRead.jsp

하지만 이렇게 처리할 경우 악의적인 사용자에 의해 잘못된 스크립트가 삽입되어 실행되는 문제가 발생할 수 있다.

예로, 글쓰기에서 새로운 게시글을 하나 만들어 글 내용에 다음과 같이 입력해 보자.

<script>
    alert(“이게 실행되면 안 되는데”);
</script>

글 읽기로 확인해 보면 자바 스크립트가 실행되어 메시지 창이 나오는 것을 알 수 있다.



HTML은 실행하고 자바 스크립트는 실행하지 못하게 해야 한다.

따라서 <script>를 &lt;script>로 바꾸어 사용자가 입력한 프로그램 코드가 실행되지 않게 만들어 주면 된다.

개발자에 따라 사용자가 입력한 내용을 DB에 저장할 때 처리하기도 하고 보여 줄 때 처리하기도 한다. 

개인적 성향으로 다음과 같이 데이터를 불러와서 보여줄 때 처리하도록 했다.

public class boardVO {
    private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag;
 ~~ 생략 ~~
    public void setBrdwriter(String brdwriter) {
        this.brdwriter = brdwriter;
    }

    public String getBrdmemo() {
        return brdmemo.replaceAll("(?i)<script", "&lt;script");
    }

boardVO.java






사용자가 게시글을 작성할 때 작성자명, 제목, 내용을 항상 입력해야 한다.

필수 입력이 되어야 하는 것이다.

모든 게시판들이 그런 것은 아니지만 본 예제는 모두 필수 입력이다.

즉, 사용자가 입력한 내용을 확인해서 내용이 있으면 서버로 보내서 저장하고,

그렇지 않으면 안내 메시지를 출력하고 입력하도록 해야 한다.



다음 코드와 같이 fn_formSubmit() 함수를 추가해 준다. 

3개의 필드를 필수로 했기 때문에 3개의 IF문이 사용되었다. 

form1은 폼테그의 이름(name)이고 폼 테그 내의 각 이름을 이용하여 값(value) 있는지 없는지 확인하고 있다.

focus메소드는 커서가 해당 입력상자에서 대기하도록 한다.

~~ 생략 ~~
<head>
<title>board3</title>
<script>
function fn_formSubmit(){
    var form1 = document.form1;
   
    if (form1.brdwriter.value=="") {
        alert("작성자를 입력해주세요.");
        form1.brdwriter.focus();
        return;
    }
    if (form1.brdtitle.value=="") {
        alert("글 제목을 입력해주세요.");
        form1.brdtitle.focus();
        return;
    }
    if (form1.brdmemo.value=="") {
        alert("글 내용을 입력해주세요.");
        form1.brdmemo.focus();
        return;
    }
    form1.submit();   
}
</script>
</head>
<body>
    ~~ 생략 ~~
        <a href="#" onclick="fn_formSubmit()">저장</a>
    </form>   
</body>
</html>

boardForm.jsp

저장 버튼도 기존에 바로 전송하도록(form1.submit()) 되어 있던 것을

코드와 같이 해당 함수에서 확인 후 처리 하도록 바꾸어 준다.


실제 사용에서는 사용자가 공백만 넣을 수도 있다.

따라서 길이를 비교하기 전에 공백을 제거하는 Trim 함수를 사용하면 좋다.

다만, 자바 스크립트의 기본 함수에는 없기 때문에 만들어 써야 한다. 

인터넷을 뒤져서 찾아보거나 패턴으로 만들어 보길 바라고,

차후 다룰 예정인 Jquery는 기본 함수로 제공되기 때문에 여기서는 넘어간다.


다음으로 글쓰기와 관련해서 수정해볼 내용은 board2에서 진행한 글쓰기와 수정을 하나의 컨트롤,

두 개의 서비스로 구현한 것을 다시 하나의 서비스로 구현해 보는 것이다. 

유사한 기능을 하는 서비스를 굳이 두 개로 사용하기 보다는

하나로 사용하는 것이 효율적이고 서비스 개념에도 맞는다고 생각한다.

다음과 같은 기존 코드에서 IF문을 서비스로 옮기면 된다.

@RequestMapping(value = "/board3Save")
public String boardSave(boardVO boardInfo) throws Exception {
    if (param.getBrdno()==null || "".equals(param.getBrdno()))
           boardSvc.insertBoard(boardInfo);
    else boardSvc.updateBoard(boardInfo);

    return "redirect:/board3List";
}

board3Ctr.java

public void insertBoard(boardVO param) throws Exception {
    if (param.getBrdno()==null || "".equals(param.getBrdno()))
           sqlSession.insert("insertBoard3", param);
    else sqlSession.update("updateBoard3", param);
}

board3Svc.java

마지막으로 위와 같이 수정했다면 updateBoard 서비스는 삭제해 준다.





+ Recent posts