thkim 2025-07-16 14:09:24 +09:00
commit 11930b8b47
11 changed files with 548 additions and 9 deletions

View File

@ -0,0 +1,26 @@
package sgis.attach.entity;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Attach {
private long attachment_id; //파일 PK
private long postId; //게시글아이디
private String fileName; //원본 파일명
private String storedFileName; //서버에 저장된 고유 파일명 (UUID등)
private String filePath; //파일 저장 경로
private long fileSize; //파일 크기(바이트)
private String fileType; //파일 MIME(타입(e.g., image/jpeg)
private long downloadCount; //다운로드횟수
private String createdAt; //생성일
}

View File

@ -0,0 +1,14 @@
package sgis.attach.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import sgis.attach.entity.Attach;
@Mapper //- Mybatis API
public interface AttachMapper {
public List<Attach> selectAttachList();
public int insertAttachList(List<Attach> attachList);
}

View File

@ -0,0 +1,21 @@
package sgis.attach.service;
import java.util.List;
import sgis.attach.entity.Attach;
public interface AttachService {
/**
* .
* @return Attach
*/
List<Attach> getLists();
/**
* .
* @param post Post
* @return Post (ID )
*/
boolean insertAttachList(List<Attach> attachList);
}

View File

@ -0,0 +1,39 @@
package sgis.attach.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sgis.attach.entity.Attach;
import sgis.attach.mapper.AttachMapper;
import sgis.attach.service.AttachService;
import java.util.List;
@Service
public class AttachServiceImpl implements AttachService {
private final AttachMapper attachMapper;
@Autowired
public AttachServiceImpl(AttachMapper attachMapper) {
this.attachMapper = attachMapper;
}
@Override
public List<Attach> getLists() {
return attachMapper.selectAttachList();
}
@Override
@Transactional
public boolean insertAttachList(List<Attach> attachList) {
int insertedRows = attachMapper.insertAttachList(attachList);
if (insertedRows > 0) {
return true;
}
return false;
}
}

View File

@ -1,11 +1,13 @@
package sgis.board.controller;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime; // OffsetDateTime import 추가
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -15,7 +17,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; // ModelAttribute import는 다른 메서드를 위해 유지
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@ -24,20 +25,23 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import egovframework.com.cmm.util.EgovUserDetailsHelper;
import sgis.attach.entity.Attach;
import sgis.attach.service.AttachService;
import sgis.board.entity.Board;
import sgis.board.entity.Member;
import sgis.board.entity.Post; // Post entity import
import sgis.board.mapper.BoardMapper;
import sgis.board.mapper.MemberMapper;
import sgis.board.mapper.PostMapper;
import sgis.board.service.PostService;
import sgis.com.util.FileUploadUtil;
import sgis.com.vo.FileVO;
import sgis.com.vo.SessionVO;
import sgis.com.web.BaseController;
@ -50,6 +54,9 @@ public class BoardRestController extends BaseController {
@Autowired
PostService postService;
@Autowired
AttachService attachService;
@Autowired
MemberMapper memberMapper;
@ -192,7 +199,8 @@ public class BoardRestController extends BaseController {
@RequestParam("boardContent") String content,
// Using @RequestParam(value = "parentPostId", required = false) for optionality.
// If it comes as an empty string, it will be null.
@RequestParam(value = "parentPostId", required = false) Long parentPostId // This is good
@RequestParam(value = "parentPostId", required = false) Long parentPostId/*, // This is good
@RequestParam("files") List<MultipartFile> files*/
) {
Post newPost = new Post();
newPost.setBoardCategoryId(boardCategoryId);
@ -233,6 +241,41 @@ public class BoardRestController extends BaseController {
// PostService를 통해 게시글 생성
Post createdPost = postService.createPost(newPost);
// 게시글 등록 성공시 파일 서버 저장 및 테이블 insert 처리
/* Date nowDate = new Date();
String strNowYyyy = new SimpleDateFormat("yyyy").format(nowDate);
String strNowMm = new SimpleDateFormat("MM").format(nowDate);
String fileUploadPath = "uploads/files/" + strNowYyyy + "/" + strNowMm + "/";
HashMap<String, Object> uploadedFileList = FileUploadUtil.multiUploadFormFile(files, fileUploadPath);
List<Attach> tbFileList = new ArrayList<Attach>(); // TB_ATTACHMENT에 insert할 리스트데이타
// 서버에 파일 업로드 처리 후, 성공하거나 util이 처리한 파일 데이타를 리턴하면 아래 수행
if (uploadedFileList != null && uploadedFileList.containsKey("file_list")) {
Object fileListObj = uploadedFileList.get("file_list");
if (fileListObj instanceof List<?>) {
List<?> fileList = (List<?>) fileListObj;
for (Object obj : fileList) {
if (obj instanceof FileVO) {
FileVO fileVO = (FileVO) obj;
Attach attach = new Attach();
attach.setPostId(createdPost.getPostId());
attach.setFileName(fileVO.getFileName()); // 원본파일명
attach.setStoredFileName(fileVO.getStoredFileName()); // 실제저장파일명
attach.setFilePath(fileVO.getFilePath()); // 파일저장경로
attach.setFileSize(fileVO.getFileSize()); // 파일크기
attach.setFileType(fileVO.getFileType()); // 파일MIME타입
tbFileList.add(attach);
}
}
}
if (tbFileList.size()>0) {
attachService.insertAttachList(tbFileList);
}
}*/
// 생성된 Post 객체 반환 (클라이언트에게 성공 여부 및 생성된 게시글 정보 전달)
return createdPost;
}

View File

@ -0,0 +1,97 @@
package sgis.com.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import egovframework.com.cmm.service.EgovProperties;
import sgis.com.vo.FileUtil;
import sgis.com.vo.FileVO;
import sgis.com.web.BaseController;
/**
* @FileName : FileUploadUtil.java
* @Date : 2025. 7. 15.
* @Creator :
* @Discription :
*/
public class FileUploadUtil extends BaseController {
//logger 설정
private final static Logger logger = LoggerFactory.getLogger(FileUploadUtil.class);
/**
* -
* @param request Multipart
* @param path uploads/files/yyyy/mm
* @return
*/
public static HashMap<String,Object> multiUploadFormFile(List<MultipartFile> files, String path) {
HashMap<String, Object> result = new HashMap<String, Object>(); // 최종리턴 파일목록(DB INSERT 할 데이타)
// HttpSession session = request.getSession();
MultipartHttpServletRequest mpRequest = (MultipartHttpServletRequest) files;
Iterator<String> fileNameIterator = mpRequest.getFileNames();
String uploadPath = EgovProperties.getProperty("Globals.File.StoredPath").replaceAll("/", "\\");
String uploadDirPath = "";
if(path.startsWith(System.getProperty("file.separator"))) {
uploadDirPath = path.substring(1).replaceAll("/", "\\");
} else {
uploadDirPath = path.replaceAll("/", "\\");
}
uploadDirPath = uploadPath + uploadDirPath;
String originalFileName = "";
String realFileName = "";
List<FileVO> fileList = new ArrayList<FileVO>();
while (fileNameIterator.hasNext()) {
//파일정보를 하나씩 취득한다
MultipartFile multiFile = mpRequest.getFile((String) fileNameIterator.next());
if (multiFile.getSize() > 0) {
originalFileName = multiFile.getOriginalFilename();
realFileName = FileUtil.getUniqueFileName(uploadDirPath, originalFileName);
File file = new File(uploadDirPath + realFileName);
// 디렉토리가 존재 하지 않을 경우 생성
if(!file.exists()) {
file.setExecutable(false, true);
file.setReadable(true);
file.setWritable(false, true);
file.mkdirs();
}
try{
multiFile.transferTo(file);
FileVO uploadFile = new FileVO();
// 파일 업로드 파일 원본파일명
uploadFile.setFileName(originalFileName);
// 파일 업로드 파일 저장파일명
uploadFile.setStoredFileName(realFileName);
// 파일 업로드 파일 저장경로
uploadFile.setFilePath(uploadDirPath);
// 파일 업로드 파일 크기
uploadFile.setFileSize(multiFile.getSize());
// 파일 업로드 파일 MIME타입
uploadFile.setFileType(multiFile.getContentType());
fileList.add(uploadFile);
} catch (IllegalStateException e) {
logger.error("IllegalStateException");
// 에러메세지리턴
} catch (IOException e) {
logger.error("IOException");
// 에러메세지리턴
}
}
}
result.put("file_list", fileList);
return result;
}
}

View File

@ -0,0 +1,63 @@
package sgis.com.vo;
import java.io.File;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author
*/
public class FileUtil {
/** 로그처리 객체 */
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
/**
*
* @param realPath
* @param fileName
* @return String
*/
public static String getUniqueFileName(String realPath, String fileName) {
int idx = 0;
int cnt = 1;
String temp = getSavedFileName(fileName); // 저장할 파일명 생성
File tempFile = new File(realPath+temp); // 기존에 동일한 파일명의 파일 존재하는지 체크
//이미존재하는 파일인지 확인
while(tempFile.exists()) {
//기존에 존재하면 확장자랑 파일명을 분리해서 [숫자]를 붙여서 파일이름을 짓는다
if((idx=fileName.indexOf(".")) != -1){
String left = fileName.substring(0, idx);
String right = fileName.substring(idx);
temp = left + "[" + cnt + "]" + right;
}else{
temp = fileName + "[" + cnt + "]";
}
tempFile = new File(realPath+temp);
cnt++;
}
//실제 저장될 파일이름 저장
fileName = temp;
return fileName;
}
public static String getSavedFileName(String originalFileName) {
// 확장자 추출
String ext = "";
int lastDot = originalFileName.lastIndexOf('.');
if (lastDot != -1) {
ext = originalFileName.substring(lastDot); // .pdf, .jpg 등
}
// UUID 생성 + 확장자 붙이기
String savedFileName = UUID.randomUUID().toString() + ext;
return savedFileName;
}
}

View File

@ -0,0 +1,36 @@
package sgis.com.vo;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* @FileName : FileVO.java
* @Date : 2025. 7. 15.
* @Creator :
* @Discription : file vo
*/
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class FileVO implements Serializable {
//serialVersionUID
private static final long serialVersionUID = 6268293686980181848L;
private long attachment_id; //파일 PK
private String postId; //게시글아이디
private String fileName; //원본 파일명
private String storedFileName; //서버에 저장된 고유 파일명 (UUID등)
private String filePath; //파일 저장 경로
private long fileSize; //파일 크기(바이트)
private String fileType; //파일 MIME(타입(e.g., image/jpeg)
private long downloadCount; //다운로드횟수
private String createdAt; //생성일
}

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="sgis.attach.mapper.AttachMapper">
<select id="selectAttachList" resultType="sgis.attach.entity.Attach">
select attachment_id,
post_id,
file_name,
stored_file_name,
file_path,
file_size,
file_type,
download_count,
TO_CHAR(created_at,'YYYY-MM-DD') AS created_at
from public.tb_attachment
</select>
<insert id="insertAttach" parameterType="sgis.attach.entity.Attach">
<selectKey keyProperty="idx" resultType="int" order="BEFORE">
SELECT COALESCE(MAX(IDX) + 1, 1) FROM TB_ATTACHMENT
</selectKey>
insert into TB_ATTACHMENT(
attachment_id
,post_id
,file_name
,stored_file_name
,file_path
,file_size
,file_type
,download_count
,created_at
) values(
#{attachment_id}
,#{post_id}
,#{file_name}
,#{stored_file_name}
,#{file_path}
,#{file_size}
,#{file_type}
,0
,NOW()
)
</insert>
<insert id="insertAttachList" parameterType="list">
<selectKey keyProperty="idx" resultType="int" order="BEFORE">
SELECT COALESCE(MAX(IDX) + 1, 1) FROM TB_ATTACHMENT
</selectKey>
insert into TB_ATTACHMENT(
attachment_id
,post_id
,file_name
,stored_file_name
,file_path
,file_size
,file_type
,download_count
,created_at
) values
<foreach item="item" collection="list" separator=",">
(
#{attachment_id}
,#{post_id}
,#{file_name}
,#{stored_file_name}
,#{file_path}
,#{file_size}
,#{file_type}
,0
,NOW()
)
</foreach>
</insert>
</mapper>

View File

@ -80,6 +80,17 @@
<td><input type="text" id="title" name="title" class="form-control"
<c:if test="${parentPostId != null && parentPostId != ''}">readonly="readonly"</c:if>/></td>
</tr>
<tr>
<th class="td-head" scope="row">파일첨부</th>
<!-- <td><input type="file" id="title" name="title" class="form-control" /></td> -->
<td>
<div id="dropZone" class="form-control p-4" style="cursor:pointer;">
<p class="text-muted">여기에 파일을 드래그 앤 드롭 하거나 클릭해서 파일 선택</p>
<input type="file" id="fileInput" multiple hidden />
<ul id="fileList" class="list-group"></ul>
</div>
</td>
</tr>
<tr>
<th class="td-head" scope="row">내용</th>
<td><textarea rows="7" class="form-control" id="boardContent" name="boardContent"></textarea></td>
@ -101,7 +112,9 @@
<!-- 쓰기 끝 -->
<script>
let fileList = []; // 파일첨부 - 첨부된 파일 목록 포시란
$(document).ready(function() {
var parentPostId = "${parentPostId}";
if (parentPostId !== null && parentPostId !== '' && parentPostId !== "null") {
// 부모글 정보 로드
@ -125,6 +138,67 @@ $(document).ready(function() {
}
});
}
const $dropZone = $("#dropZone"); // 파일첨부 - 드래그앤드롭
const $fileInput = $("#fileInput"); // 파일첨부 -
const $fileListUI = $("#fileList"); // 첨부된 파일 목록 표시
// 파일 선택 창 열기
$dropZone.on("click", function () {
$fileInput.trigger("click");
});
// 드래그 오버 스타일
$dropZone.on("dragover", function (e) {
e.preventDefault();
$dropZone.addClass("dragover");
});
$dropZone.on("dragleave", function () {
$dropZone.removeClass("dragover");
});
$dropZone.on("drop", function (e) {
e.preventDefault();
$dropZone.removeClass("dragover");
const files = e.originalEvent.dataTransfer.files;
handleFiles(files);
});
$fileInput.on("change", function () {
handleFiles(this.files);
});
function handleFiles(files) {
$.each(files, function (i, file) {
// 중복 체크는 이름으로만 (간단히 처리)
if (!fileList.some(f => f.name === file.name && f.size === file.size)) {
fileList.push(file);
}
});
renderFileList();
}
function renderFileList() {
$fileListUI.empty();
$.each(fileList, function (index, file) {
const $li = $(`
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>` + file.name + ` </span>
<b class="icon-trash remove-btn" title="삭제버튼" style="cursor:pointer;">삭제</b>
</li>
`);
$li.find(".remove-btn").on("click", function () {
removeFile(index);
});
$fileListUI.append($li);
});
}
function removeFile(index) {
fileList.splice(index, 1);
renderFileList();
}
});
function goList(){
@ -171,4 +245,49 @@ function goInsert(){
});
$(".fclear").trigger("click");
}
function addPost(){
var title = $("#title").val();
var content = $("#boardContent").val();
var boardCategoryId = $("#boardCategoryId").val();
var parentPostId = $("#parentPostId").val();
if(title == null || title == "" || title == 0){
alert("제목을 입력하세요");
return false;
} else if (content == null || content == "" || content == 0) {
alert("내용을 입력하세요");
return false;
}
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
formData.append("boardCategoryId", boardCategoryId);
if (parentPostId !== null && parentPostId !== "" && parentPostId !== "null") {
formData.append("parentPostId", parentPostId);
}
// $.each(fileList, function (i, file) {
// formData.append("files", file);
// });
$.ajax({
url : "/sgis/portal/board/new.do",
type : "post",
data: formData,
contentType: false,
processData: false,
success : function(){
$("#grid").empty();
var categoryId = "${boardCategoryId}" ? "${boardCategoryId}" : "${params.boardCategoryId}";
location.href = "/sgis/portal/board-list.do?boardCategoryId=" + categoryId;
},
error : function() {
alert("error");
}
});
$(".fclear").trigger("click");
}
</script>

View File

@ -1806,7 +1806,12 @@ footer .newsletter input { color: #6f6f6f; letter-spacing: normal; }
.table .input {margin: 0px;}
.table textarea {display: block;}
.table #dropZone { height: auto !important; position: relative; }
.table #dropZone p { padding: 10px; }
.table #fileInput { position: absolute; top:0; left: 0; width: 100%; height: 100%; opacity: 0; }
.table #dropZone.dragover { background-color: #e9f7fe; border-color: #17a2b8; }
.table #dropZone #fileList { margin-bottom: 0}
.table .remove-btn { background: url(../img/common/icon/ico_btn_delete_s.png) no-repeat 10% 50%; display: inline-block; padding: 6px 0px 0px 23px; width: 60px; height: 30px; background-color: #a5d0e0; color: #fff; vertical-align: middle; border-radius: 6px;}
.table-content-item {min-height: 200px;}
.table-top-btn-group {position: relative; width: 100%; display: table; box-sizing: border-box; text-align: right; padding-bottom: 20px;}
.table-bottom-btn-group {position: relative; width: 100%; display: table; box-sizing: border-box; text-align: right; padding-top: 20px;}