포스트

GPR 분석공동조사서 자동화 프로그램 개발 프로젝트

JS-document : 분석공동조사서 양식 자동 채움 프로그램

  • Tool :
    Spring Spring Tool Suite 4


🔔 JS-document 소개

📌 개발 동기

  • 현재 재직중인 회사는 싱크홀(공동) 조사 서비스를 제공합니다.
  • 싱크홀 현장 조사를 위해서는 이미지가 첨부된 조사서가 필요합니다.
  • 조사서를 제작하기 위해서는 단순 이미지 첨부 작업 등이 필요합니다.
  • 조사서 1개를 제작하는 데 소요되는 시간은 10~20분 정도입니다.
  • 1년에 제작해야 될 조사서 개수는 800~1000개 정도입니다.
  • 즉, 조사서 제작을 자동화하면 최대 300시간 이상을 절약할 수 있습니다.
  • Spring 학습 및 향후 조사서 작업 시간 단축을 위해 개발을 시작하였습니다.

📌 주요 기능

  • 조사서 기본 틀에 이미지를 삽입할 수 있습니다.
  • 이미지는 각 테이블 공간에 파일 형태로 삽입 가능합니다.
  • 또한 폴더를 업로드 하면 폴더 내 이미지가 자동으로 삽입됩니다.
  • 이미지를 클릭하면 수정/삭제가 가능합니다.

📌 추후 개발 예정 내용

  • 웹 프로그램에서 작성한 내용을 HWP, PDF, DOCX 파일로 변환하려고 합니다.
  • 작성된 조사서를 게시판 형태(업로드, 수정, 삭제, 접근)로 관리하려고 합니다.
  • 링크된 깃허브 레포지토리를 클릭하시면 깃허브 레포지토리 페이지로 전환됩니다.
figure
싱크홀 조사를 위한 분석공동조사서 예시
figure
JS-document 초기 화면


🔔 개발 환경 설정

📌 IDE

  • JS-document는 웹 프로그램으로 구현하였습니다.
  • JS-document 개발을 위해 Java Enterprise Editon을 사용하였습니다.
  • Java EE 사용을 위해 Spring Tool Suite 4를 사용하였습니다.
  • Spring Boot를 사용하지 않은 이유는 Legacy 방식을 학습하기 위해서입니다.

📌 Maven Project

  • JS-document를 개발하기 위해 Maven Project를 생성하였습니다.
  • Maven Project를 선택한 이유는 표준화된 구조를 학습하기 위해서입니다.
  • 예를들면 구조화 된 디렉토리 구조나 pom.xml을 경험하고 싶었습니다.

📌 Maven Project 구조 설정

  • Maven Project를 구동시키기 위한 세부적인 코드는 타인의 코드를 인용하였습니다.
  • pom.xml : 프로젝트 의존성을 설정하기 위해 참조하였습니다.
  • root-context.xml : MyBatis 설정 및 데이터베이스 연동을 위해 참조하였습니다.
  • servlet-context.xml : ViewResolver 객체 설정을 위해 참조하였습니다.
  • web.xml : DispatcherServlet 설정을 위해 참조하였습니다.
  • mybatis-config.xml : 데이터베이스 매핑 설정을 위해 참조하였습니다.


🔔 구현 과정

📌 JSP 페이지 작성(document.jsp)

  • 사용자에게 보여질 웹 페이지는 JSP를 이용하여 구현하였습니다.
  • JSP 페이지에 포함되는 내용은 아래와 같습니다.
    • 조사서 템플릿을 구성하기 위한 table 태그
    • 이미지 첨부를 위한 input 태그
    • 테이블 및 이미지 규격을 정의하기 위한 CSS
    • 이미지 파일 업로드를 위한 script 태그
    • 폴더 내 이미지 업로드를 위한 script 태그
  • 참고로 별도의 Java 코드는 사용하지 않았기 때문에 반드시 JSP일 필요는 없었습니다.
  • 아래는 document.jsp의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>js-document</title>
<!-- 테이블 규격 -->
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/tableSet.css">
<!-- 요소 아이디별 이미지 스타일 -->
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssLocation.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssFront.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssBack.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssLeft.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssRight.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssFlatSection.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssLongSection.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssCrossSection.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssCavity.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/cssSurface.css">
<style>
	/* 기본 스타일 */
	@font-face {
        font-family: "NanumMyeongjo";
        src: url("resources/fonts/NanumMyeongjo.otf") format("truetype");
    }
    
	body {
		margin: 0;
		padding: 0;
		font-family: "NanumMyeongjo", NanumMyeongjo;
	}
	
	img {
		margin: 0;
		padding: 0;
		display: block;
		width: 100%;
		height: 100%;
	}
	
	.main {
		margin: 5%; /* 내비게이터가 위치할 곳 */
		justify-content: center;
		display: flex;
	}
		
	.table {
		table-layout: auto;
		border-bottom: 1px solid black;
		border-collapse: collapse;
		border-spacing: 0;
	}
	
	.inner-table {
		font-size: 10pt;
		position: absolute;
		background-color: white;
		border-collapse: collapse;
		margin-top: -8mm;
		margin-left: 214.85mm;
		z-index: 1;
	}
	
	.table td {
		border: 1px solid black;
		padding: 0;
	}
	
	
	
	/* 세부 스타일 */
	.title {
		position: relative;
		width: 266.06mm;
		height: 11.4mm;
		font-size: 17pt;
		text-align: center;
		border: none;
	}
	.title::after {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		bottom: 1.5mm;
		border-bottom: 1.2px solid black;
		width: 17%;
		margin: 0 auto;
	}
	.title::before {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		bottom: 1.2mm;
		border-bottom: 1.2px solid black;
		width: 17%;
		margin: 0 auto;
	}
	
	.sub-title {
		background-color: #f2f2f2;
	}
	
	.document-text td {
		text-align: center;
		white-space: nowrap;
	}
	
	.strong-row td {
		background-color: #f2f2f2;
	}
	
	.button {
		 display: flex;
		 justify-content: center;
		 align-items: center;
		 
		 margin: auto;
		 padding: 0;
		 width: 100px;
		 height: 30px;
		 color: black;
		 background-color: #f2f2f2;
		 cursor: pointer;
		 border-radius: 5px;
		 border: 1px solid black;
	}
	
	.button:hover {
		background: #ddd;
	}
	
	.interface-folder {
		margin: 0;
		padding: 0;
		background-color: skyblue;
		cursor: pointer;
	}
</style>
</head>
<body>
	<section class="container">
		<!-- 폴더 업로드 버튼 -->
		<div>
			<input type="file" id="folder-input" class="interface-folder" webkitdirectory directory multiple>
		</div>
		<div>
			<button onclick="location.href='http://localhost:9090/word.do';">WORD 변환</button>
		</div>
		<div class="main" id="full-document">
			<table class="table total-size">
				<tbody class="document-text">
					<tr>
						<th colspan="11" class="title"><strong>분석 공동조사서</strong>
							<table class="inner-table">
								<tr>
									<td class="record-1"><strong>공동관리번호</strong></td>
									<td class="record-1"></td>
								</tr>
							</table>
						</th>
					</tr>
					<tr>
						<td colspan="1" class="record-2-1 sub-title">탐사 / 천공</td>
						<td colspan="1" class="record-2-2">탐사일  2022.02.15 / 천공일 -</td>
						<td colspan="1" class="record-2-3 sub-title" rowspan="4"><p class="gap-2-3"></p></td>
						<td colspan="1" class="record-2-4 sub-title">위도</td>
						<td colspan="1" class="record-2-5">37.816955</td>
						<td colspan="1" class="record-2-6 sub-title" rowspan="4"><p class="gap-2-6"></p><p class="gap-2-6"></p><p class="gap-2-6"></p></td>
						<td colspan="1" class="record-2-7 sub-title">토피(㎝)</td>
						<td colspan="1" class="record-2-8">53</td>
						<td colspan="1" class="record-2-9 sub-title" rowspan="2">공동<span class="gap-2-9"></span>관리</td>
						<td colspan="1" class="record-2-10 sub-title">번호</td>
						<td colspan="1" class="record-2-11">-</td>
					</tr>
					<tr>
						<td colspan="1" class="record-2-1 sub-title">관  할  구</td>
						<td colspan="1" class="record-2-2">백석읍</td>
						<td colspan="1" class="record-2-4 sub-title">경도</td>
						<td colspan="1" class="record-2-5">126.947396</td>
						<td colspan="1" class="record-2-7 sub-title">도로종단 길이(㎝)</td>
						<td colspan="1" class="record-2-8">95</td>
						<td colspan="1" class="record-2-10 sub-title">등급</td>
						<td colspan="1" class="record-2-11">-</td>
					</tr>
					<tr>
						<td colspan="1" class="record-2-1 sub-title">도로명 주소</td>
						<td colspan="1" class="record-2-2">연곡리 327</td>
						<td colspan="1" class="record-2-4 sub-title">차선</td>
						<td colspan="1" class="record-2-5">중앙선 기준 1차로</td>
						<td colspan="1" class="record-2-7 sub-title">도로횡단 폭(㎝)</td>
						<td colspan="1" class="record-2-8">69</td>
						<td colspan="1" class="record-2-9 sub-title" rowspan="2">분석<span class="gap-2-9"></span>공동</td>
						<td colspan="1" class="record-2-10 sub-title">번호</td>
						<td colspan="1" class="record-2-11">038-1</td>
					</tr>
					<tr>
						<td colspan="1" class="record-2-1 sub-title">탐 사 방 향</td>
						<td colspan="1" class="record-2-2">비암리 29-1→연곡리 327-2</td>
						<td colspan="1" class="record-2-4 sub-title">지점</td>
						<td colspan="1" class="record-2-5">정지선 후방 8.91m</td>
						<td colspan="1" class="record-2-7 sub-title">바닥 깊이(㎝)</td>
						<td colspan="1" class="record-2-8">-</td>
						<td colspan="1" class="record-2-10 sub-title">등급</td>
						<td colspan="1" class="record-2-11">-</td>
					</tr>


					<!-- location data -->
					<tr class="strong-row">
						<td colspan="5" class="record-3-1"><strong>공동 위치도</strong></td>
						<td colspan="6" class="record-3-2" colspan="2"><strong>공동 주변사진</strong></td>
					</tr>
					<tr>
						<td colspan="5" class="record-4-1 img-area" id="css-location" data-img-key="location" rowspan="2">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-location" multiple accept="image/*" style="display: none">
								<label class="button" for="img-location">이미지 선택</label>
								
							</div>
						</td>
						<td colspan="3" class="record-4-2 img-area" id="css-front" data-img-key="front">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-front" multiple accept="image/*" style="display: none">
								<label class="button" for="img-front">이미지 선택</label>
							</div>
						</td>
						<td colspan="3" class="record-4-2 img-area" id="css-back" data-img-key="back">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-back" multiple accept="image/*" style="display: none">
								<label class="button" for="img-back">이미지 선택</label>
							</div>
						</td>
					</tr>
					<tr>
						<td colspan="3" class="record-4-2 img-area" id="css-left" data-img-key="left">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-left" multiple accept="image/*" style="display: none">
								<label class="button" for="img-left">이미지 선택</label>
							</div>
						</td>
						<td colspan="3" class="record-4-2 img-area" id="css-right" data-img-key="right">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-right" multiple accept="image/*" style="display: none">
								<label class="button" for="img-right">이미지 선택</label>
							</div>
						</td>
					</tr>


					<!-- cavity data -->
					<tr class="strong-row">
						<td colspan="4" class="record-5-1"><strong>(상단) 탐사영상 평면 / (하단) 노면영상</strong></td>
						<td colspan="2" class="record-5-2"><strong>종단면</strong></td>
						<td colspan="2" class="record-5-3"><strong>횡단면</strong></td>
						<td colspan="3" class="record-5-4"><strong>공동확인 내시경 영상</strong></td>
					</tr>
					<tr>
						<td colspan="4" class="record-6-1 img-area" id="css-flat-section" data-img-key="flat-section">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-flat-section" multiple accept="image/*" style="display: none">
								<label class="button" for="img-flat-section">이미지 선택</label>
							</div>
						</td>
						<td colspan="2" class="record-6-2 img-area" rowspan="2" id="css-long-section" data-img-key="long-section">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-long-section" multiple accept="image/*" style="display: none">
								<label class="button" for="img-long-section">이미지 선택</label>
							</div>
						</td>
						<td colspan="2" class="record-6-3 img-area" rowspan="2" id="css-cross-section" data-img-key="cross-section">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-cross-section" multiple accept="image/*" style="display: none">
								<label class="button" for="img-cross-section">이미지 선택</label>
							</div>
						</td>
						<td colspan="3" class="record-6-4 img-area" rowspan="2" id="css-cavity" data-img-key="cavity">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-cavity" multiple accept="image/*" style="display: none">
								<label class="button" for="img-cavity">이미지 선택</label>
							</div>
						</td>
					</tr>
					<tr>
						<td colspan="4" class="record-6-5 img-area" id="css-surface" data-img-key="surface">
							<div>
								<h4>이미지를 가져오세요.</h4>
								<img style="display: none">
								<input type="file" id="img-surface" multiple accept="image/*" style="display: none">
								<label class="button" for="img-surface">이미지 선택</label>
							</div>
						</td>
					</tr>
				</tbody>
			</table>
		</div>
	</section>
	<section class="helper">
		<div class="readme">
			<p class="readme-title">이미지 첨부 방법</p>
			<p class="readme-contents">1. 이미지가 담긴 폴더를 드래그하여 일괄 첨부한다.</p>
			<p class="readme-contents">2. 이미지를 직접 드래그하여 개별 첨부한다.</p>
			<p class="readme-contents">3. 이미지 선택 버튼을 클릭하여 개별 첨부한다.</p>
		</div>
		
		<!-- PDF 변환 버튼 -->
		<button onclick="getPDF();">PDF 변환</button>
		
		<!-- DOCX 변환 버튼 -->
		<button onclick="getDocx()">WORD 변환</button>
	</section>
</body>
<!-- 드래그를 이용한 이미지 파일 업로드 기능 -->
<script src="${pageContext.request.contextPath}/resources/js/imgUploadHandler.js"></script>
<!-- 드래그를 이용한 폴더 내 파일 업로드 기능 -->
<script src="${pageContext.request.contextPath}/resources/js/folderUploadHandler.js"></script>
<!-- 클라이언트 사이드에서의 PDF 변환 기능 -->
<script src="resources/lib/jspdf.min.js"></script>
<script src="resources/lib/html2canvas.min.js"></script>
<script src="${pageContext.request.contextPath}/resources/js/pdfGenerator.js"></script>
<!-- 클라이언트 사이드에서의 DOCX 변환 기능 -->
</html>

📌 테이블 규격(tableSet.css)

  • 조사서 양식의 테이블을 구현하기 위해 세부적인 규격을 설정하였습니다.
  • 각각의 규격은 레코드별로 세부적으로 정의하여 오차를 최소화하였습니다.
  • tableSet.css 파일은 JSP 파일과 분리하여 관리하였고 stylesheet로 참조하였습니다.
  • 참고로 조사서 양식은 서울시청에서 제공한 것입니다.
  • 아래는 tableSet.css의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@charset "UTF-8";

/* 테이블 규격 스타일 */
.total-size {
	width: 261.00mm;
	height: 162.44mm;
}

.record-1 {
	width: 25mm;
	height: 8mm;
}

.record-2-1 {
	width: 22.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-2 {
	width: 80.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-3 {
	width: 7.00mm;
	height: 22.32mm;
	font-size: 10.5pt;
}

.gap-2-3 {
	margin-top: 2rem;
}

.record-2-4 {
	width: 9.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-5 {
	width: 45.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-6 {
	width: 7.00mm;
	height: 22.32mm;
	font-size: 10.5pt;
}

.gap-2-6 {
	margin-top: -0.6rem;
}

.record-2-7 {
	width: 27.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-8 {
	width: 16.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-9 {
	width: 9.00mm;
	height: 11.16mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.gap-2-9 {
	display: block;
	margin: 5px 0;
}

.record-2-10 {
	width: 9.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-2-11 {
	width: 30.00mm;
	height: 5.58mm;
	font-size: 10.5pt;
	letter-spacing: -0.03em;
}

.record-3-1 {
	width: 165.00mm;
	height: 7.06mm;
}

.record-3-2 {
	 width: 96.00mm;
	 height: 7.06mm;
}

.record-4-1 {
	width: 169.34mm;
	height: 57.08mm;
}

.record-4-2 {
	width: 48.00mm;
	height: 30.00mm;
}

.record-5-1 {
	width: 140.30mm;
	height: 7.06mm;
}

.record-5-2 {
	width: 45.70mm;
	height: 7.06mm;
}

.record-5-3 {
	width: 27.00mm;
	height: 7.06mm;
}

.record-5-4 {
	width: 48.00mm;
	height: 7.06mm;
}

.record-6-1 {
	width: 140.30mm;
	height: 22.00mm;
}

.record-6-2 {
	width: 45.70mm;
	height: 55.00mm;
}

.record-6-3 {
	width: 27.00mm;
	height: 55.00mm;
}

.record-6-4 {
	width: 48.00mm;
	height: 55.00mm;
}

.record-6-5 {
	width: 140.30mm;
	height: 33.00mm;
}

📌 이미지 업로드 기능 부여(imgUploadHandler.js)

  • 이미지 업로드를 위해 드로그앤드롭 기능을 부여하였습니다.
  • 또한 파일 선택기를 이용한 파일 업로드 기능도 부여하였습니다.
  • 이미지가 업로드 되면 getElementById로 위치 및 규격이 자동으로 설정됩니다.
  • 업로드 된 이미지를 클릭하면 수정 및 삭제가 가능하도록 하였습니다.
  • 아래는 imgUploadHandler.js의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/* 이미지 업로드 기능 */

/* 파일 드래그 업로드 기능 */
// HTML 파싱 및 DOM 트리 구축 이후 콜백 함수를 실행하도록 설정
document.addEventListener('DOMContentLoaded', function() {
	// 클래스가 img-area인 모든 HTML 요소들의 컬렉션 구축
    const uploadAreas = document.querySelectorAll('.img-area');
	// HTML 컬렉션의 각 요소에 대한 이벤트 리스너 설정
	// 개별 ID 마다의 JavaScript 코드 필요성 소멸
    uploadAreas.forEach(area => {
		/* 드래그를 이용한 파일 업로드 시스템 */
        // 정상적인 파일 드롭 기능 수행을 위한 브라우저의 기본 동작 방지
        area.addEventListener('dragover', function(e) {
            e.preventDefault();
        });

        // 정상적인 파일 드롭 기능 수행을 위한 브라우저의 기본 동작 방지
        area.addEventListener('drop', function(e) {
            e.preventDefault();
			// 파일 목록을 포함하는 files 객체 설정
			// 이벤트 발생 시 dataTransfer 객체로 데이터에 접근 
            const files = e.dataTransfer.files;
			// files 및 이미지 영역의 ID를 매개변수로 적용
			// uploadFiles 함수를 이용하여 드롭된 이미지를 페이지에 표시
            uploadFiles(files, area.id);
        });

		/* 파일 선택기를 이용한 직접적인 파일 업로드 시스템 */
		// querySelector를 이용한 area 요소의 파일 입력 필드 탐색
		// 파일 입력 필드에서 change 이벤트 발생 시 콜백 함수 실행
        area.querySelector('input[type="file"]').addEventListener('change', function(e) {
			// 이벤트 객체의 target 속성으로 파일 입력 필드 참조
            const files = e.target.files;
			// files 및 이미지 영역의 ID를 매개변수로 적용
			// uploadFiles 함수를 이용하여 드롭된 이미지를 페이지에 표시
            uploadFiles(files, area.id);
        });
    });

	/* 파일 선택 업로드 기능 */
    function uploadFiles(files, areaId) {
		// HTML 문서에서 개별 ID 요소를 dropArea 변수에 할당
        var dropArea = document.getElementById(areaId);
		// 이미지 영역 초기화 후 새 이미지 표시
        dropArea.innerHTML = '';

		// files 객체를 배열로 변환
		// 선택된 모든 파일에 대해 함수 실행
        Array.from(files).forEach(file => {
			// 파일 처리를 위한 FileReader 객체 생성
            var reader = new FileReader();
			// 파일 데이터가 로드되면 콜백 함수 호출
            reader.onload = function(e) {
				// 새로운 Image 객체 생성
                var img = new Image();
				// img 요소의 src 속성에 파일 데이터 할당
                img.src = e.target.result;
				// 이미지 클릭 시 removeImage 함수 호출
				img.onclick = removeImage;
				// img 요소의 배치 설정
                img.style.position = 'absolute';
				// img 요소의 상부 위치 설정
                img.style.top = '0';
				// img 요소의 좌측 위치 설정
                img.style.left = '0';
				// img 요소의 전체 너비 사용
                img.style.width = '100%';
				// img 요소의 전체 높이 사용
                img.style.height = '100%';
				// 설정된 이미지 요소를 dropArea 변수에 추가
                dropArea.appendChild(img);
            };
			// 데이터를 URL 형태로 변환
            reader.readAsDataURL(file);
        });
    }
	
	/* 이미지 삭제 기능 */
	// 이미지 클릭 시 실행될 removeImage 함수 정의
	function removeImage(e) {
		// 이미지 삭제 여부 대화 상자 표시
		if(confirm('이미지를 삭제하시겠습니까?')) {
			// 선택된 이미지를 img 변수에 할당
			var img = e.target;
			// 이미지 삭제 전 이미지 영역의 ID 추출
			var areaId = img.closest('.img-area').id;
			// remove 메서드를 호출하여 DOM에서 이미지 요소 삭제
			img.remove();
			// 이미지 삭제 알림창 표시
			alert('이미지가 삭제되었습니다.');
			// 이미지 삭제 후 resetDropArea 함수 호출
			resetDropArea(areaId);
		} else {
			// 이미지 삭제 취소 알림창 표시
			alert('이미지 삭제가 취소되었습니다.');
		}
	}
	
	/* 이미지 영역 리셋 기능 */
	// resetDropArea 함수 정의
	function resetDropArea(areaId) {
		// ID 요소를 dropArea 변수에 재할당
		var dropArea = document.getElementById(areaId);
		// 이미지 영역의 내부 HTML 설정
		dropArea.innerHTML = '<h4>이미지를 가져오세요.</h4>';
		
		/* file 타입의 input 요소 재생성 */
		var input = document.createElement('input');
		// input 요소의 타입 지정
		input.type = 'file';
		// input 요소의 id 지정
		input.id = areaId + '-file';
		// 여러 파일 선택이 가능하도록 설정
		input.multiple = true;
		// 모든 이미지 확장자에 대한 선택이 가능하도록 설정
		input.accept = 'image/*';
		// 이미지 업로드 되기 전까지 요소가 표시되지 않도록 설정
		input.style.display = 'none';
		// 설정을 마친 input 요소를 dropArea 변수에 추가
		dropArea.appendChild(input);
		
		/* label 요소 재생성 */
		var label = document.createElement('label');
		// label 요소의 class 속성 설정
		label.className = 'button';
		// label 요소의 for 속성에 값 할당
		label.setAttribute('for', input.id);
		// label 요소의 텍스트 내용 설정
		label.textContent = '이미지 선택';
		// 설정을 마친 label 요소를 dropArea 변수에 추가
		dropArea.appendChild(label);
		
		/* 파일 업로드 함수 호출 */
		document.addEventListener('change', function(e) {
			// 이벤트 객체의 target 속성으로 파일 입력 필드 참조
            const files = e.target.files;
			// files 및 이미지 영역의 ID를 매개변수로 적용
			// uploadFiles 함수를 이용하여 드롭된 이미지를 페이지에 표시
            uploadFiles(files, areaId);
		});
	}
});

📌 폴더 내 이미지 업로드 기능 부여(folderUploadHandler.js)

  • 파일 형태만이 아닌 폴더 형태로도 이미지 업로드를 가능하게 하였습니다.
  • 폴더 업로드 기능 또한 파일 선택기를 이용할 수 있도록 하였습니다.
  • fileToImgKeyMap을 이용하여 이미지의 위치와 파일 이름을 매핑하였습니다.
  • 그래서 폴더가 업로드 되면 이미지의 위치 및 규격이 자동으로 설정됩니다.
  • 업로드 된 이미지를 클릭하면 수정 및 삭제가 가능하도록 하였습니다.
  • 아래는 folderUploadHandler.js의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* 폴더 업로드 기능 */
document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('folder-input').addEventListener('change', function(event) {
        const files = event.target.files;

        // 파일이 업로드될 위치의 ID를 파일 이름과 매핑
		// 자동 저장되는 파일 이름을 기반으로 img 태그의 ID를 매핑
        const fileToImgKeyMap = {
            'front': 'front',
            'left': 'left',
            'map': 'location',
            'rear': 'back',
            'right': 'right',
            'road': 'surface',
            'xy': 'flat-section',
            'xz': 'cross-section',
            'yz': 'long-section',
			'cavity' : 'cavity'
        };
		
		/* 폴더 내부 이미지 파일 설정 */
		Array.from(files).forEach(file => {
			// toLowerCase() = 파일 이름을 소문자로 변환
			// split('.') = 문자열의 이미지 파일 이름을 점을 기준으로 분할
			// [0] = 분할된 문자열의 첫 번째 요소 참조
            let fileKey = file.name.toLowerCase().split('.')[0];
			
			// data-img-key 속성에 해당되는 이미지 영역 탐색
			let imgKey = fileToImgKeyMap[fileKey];
			if (imgKey) {
				// imgKey = 매핑된 HTML 데이터에 기반
				// fileKey = 파일 이름에 기반
				document.querySelectorAll(`.img-area[data-img-key="${imgKey}"]`).forEach(area => {
					const placeholderString = area.querySelector('h4');
					const placeholderLabel = area.querySelector('label');
					const img = area.querySelector('img');
					
					if (img) {
						img.style.display = 'block';
						img.src = URL.createObjectURL(file);
						img.onload = () => URL.revokeObjectURL(img.src);
						// 이미지 클릭 시 removeImage 함수 호출
						img.onclick = removeImage;
						// img 요소의 배치 설정
		                img.style.position = 'absolute';
						// img 요소의 상부 위치 설정
		                img.style.top = '0';
						// img 요소의 좌측 위치 설정
		                img.style.left = '0';
						// img 요소의 전체 너비 사용
		                img.style.width = '100%';
						// img 요소의 전체 높이 사용
		                img.style.height = '100%';
					}
					
					// h4 태그에 포함되는 텍스트 숨기기
					if (placeholderString) {
						placeholderString.style.display = 'none';
					}
					
					// label 태그에 포함되는 텍스트 숨기기
					if (placeholderLabel) {
						placeholderLabel.style.display = 'none';
					}
				});
			}
		});
		
		/* 이미지 삭제 기능 */
		// 이미지 클릭 시 실행될 removeImage 함수 정의
		function removeImage(e) {
			// 이미지 삭제 여부 대화 상자 표시
			if (confirm('이미지를 삭제하시겠습니까?')) {
				// 선택된 이미지를 img 변수에 할당
				var img = e.target;
				// 이미지 삭제 전 이미지 영역의 ID 추출
				var areaId = img.closest('.img-area').id;
				// remove 메서드를 호출하여 DOM에서 이미지 요소 삭제
				img.remove();
				// 이미지 삭제 알림창 표시
				alert('이미지가 삭제되었습니다.');
				// 이미지 삭제 후 resetDropArea 함수 호출
				resetDropArea(areaId);
			} else {
				// 이미지 삭제 취소 알림창 표시
				alert('이미지 삭제가 취소되었습니다.');
			}
		}
		
		/* 이미지 영역 리셋 기능 */
		// resetDropArea 함수 정의
		function resetDropArea(areaId) {
			// ID 요소를 dropArea 변수에 재할당
			var dropArea = document.getElementById(areaId);
			// 이미지 영역의 내부 HTML 설정
			dropArea.innerHTML = '<h4>개별 이미지를 가져오세요.</h4>';
			
			/* file 타입의 input 요소 재생성 */
			var input = document.createElement('input');
			// input 요소의 타입 지정
			input.type = 'file';
			// input 요소의 id 지정
			input.id = areaId + '-file';
			// 여러 파일 선택이 가능하도록 설정
			input.multiple = true;
			// 모든 이미지 확장자에 대한 선택이 가능하도록 설정
			input.accept = 'image/*';
			// 이미지 업로드 되기 전까지 요소가 표시되지 않도록 설정
			input.style.display = 'none';
			// 설정을 마친 input 요소를 dropArea 변수에 추가
			dropArea.appendChild(input);
		}
    });
});

📌 PDF 변환 기능(pdfGenerator.js; 미완성)

  • 클라이언트 사이드에서 HTML 문서가 PDF로 변환되게 설정하였습니다.
  • PDF 변환을 이를 위해 html2canvas 라이브러리를 이용하였습니다.
  • 하지만 변환된 PDF의 품질이 좋지 않았습니다.
  • 이유는 HTML 문서가 복잡한 표로 구성된 것으로 예상됩니다.
  • 서버 사이드에서도 구현을 시도해보았지만 품질이 좋지 않았습니다.
  • 추후 기능을 완성하여 업데이트 할 예정입니다.
  • 아래는 pdfGenerator.js의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* PDF 변환 기능 */

// 클라이언트 사이드에서 PDF 변환 버튼을 누르면 실행되도록 설정

function getPDF() {
	if (typeof html2canvas === "undefined" || typeof jsPDF === "undefined") {
        alert("PDF 생성에 문제가 발생되었습니다. 페이지 새로고침 바랍니다.");
		return;
    }
	
    html2canvas(document.getElementById('full-document')).then(canvas => {
        const imgData = canvas.toDataURL('image/png');
        const pdf = new jsPDF({
        	orientation: 'landscape',
            unit: 'mm',
            format: 'a4',
        });
	   
        const imgProps = pdf.getImageProperties(imgData);
        const pdfWidth = pdf.internal.pageSize.getWidth() - 30;
        const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
        
        pdf.addImage(imgData, 'PNG', 15, 17, pdfWidth, pdfHeight);
        pdf.save("download.pdf");
        
    }).catch(error => {
    	console.error("PDF 생성 오류 발생 : ", error);
    	alert("PDF 생성에 문제가 발생되었습니다. 콘솔창 확인 바랍니다.");
    });
}

📌 DOCX 변환 기능(wordGenerator.js; 미완성)

  • 워드 파일 변환 구현을 시도해보았습니다.
  • 하지만 PDF 변환의 부실함과 마찬가지로 품질이 좋지 않았습니다.
  • 추후 기능을 완성하여 업데이트 할 예정입니다.
  • 아래는 wordGenerator.js의 전체 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* WORD 변환 기능 */

// 클라이언트 사이드에서 WORD 변환 버튼을 누르면 실행되도록 설정

function getDocx() {
    var content = document.getElementById('full-document').outerHTML;
    if (!content) {
        alert('문서를 변환할 수 있는 영역을 찾을 수 없습니다.');
        return;
    }
    var converted = htmlDocx.asBlob(content, {
        orientation: 'portrait',
        margins: { top: 720 }
    });

    if (!converted) {
        alert('문서 변환 중 문제가 발생했습니다.');
        return;
    }

    saveAs(converted, '분석공동조사서.docx');
}


🔔 추후 개발 예정 내용

📌 PDF, DOCX, HWP 변환 기능

  • PDF 및 워드 파일 기능을 업데이트 할 예정입니다.
  • 한글 파일 변환의 경우 대금 발생 가능성이 있습니다.
  • 그래서 기능 추가를 위한 추가적인 파악이 필요합니다.

📌 게시판 형태

  • 조사서 생성 페이지를 포함한 게시판을 제작할 예정입니다.
  • 개발될 게시판은 조사서 생성, 수정, 삭제 등의 접근이 가능합니다.
  • 또한 파일 업로드, 수정, 삭제를 통한 조사서 관리가 가능합니다.
  • 게시판에는 회원만 접근할 수 있도록 설정할 예정입니다.
  • 회원 관리는 MariaDB를 이용할 예정입니다.




이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
<>