프로젝트 기초 설정 업데이트

This commit is contained in:
USER
2026-08-21 11:19:42 +09:00
parent e55ef16ac9
commit 529de50dab
15 changed files with 631 additions and 0 deletions
@@ -0,0 +1,54 @@
package kr.itpinc.first.common.common;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class CommandMap {
Map<String,Object> map = new HashMap<String,Object>();
public Object get(String key){
return map.get(key);
}
public void put(String key, Object value){
map.put(key, value);
}
public Object remove(String key){
return map.remove(key);
}
public boolean containsKey(String key){
return map.containsKey(key);
}
public boolean containsValue(Object value){
return map.containsValue(value);
}
public void clear(){
map.clear();
}
public Set<Entry<String, Object>> entrySet(){
return map.entrySet();
}
public Set<String> keySet(){
return map.keySet();
}
public boolean isEmpty(){
return map.isEmpty();
}
public void putAll(Map<? extends String, ?extends Object> m){
map.putAll(m);
}
public Map<String,Object> getMap(){
return map;
}
}
@@ -0,0 +1,47 @@
package kr.itpinc.first.common.controller;
import java.net.URLEncoder;
import java.util.Map;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import kr.itpinc.first.common.common.CommandMap;
import kr.itpinc.first.common.service.CommonService;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class CommonController
{
@Autowired
private CommonService commonService;
@RequestMapping(value="/common/downloadFile.do")
public void downloadFile(CommandMap commandMap, HttpServletResponse response) throws Exception
{ //서버에서 화면으로 응답할때 response
Map<String, Object> map = commonService.selectFileInfo(commandMap.getMap());
String storedFileName = (String)map.get("STORED_FILE_NAME");
String originalFileName = (String)map.get("ORIGINAL_FILE_NAME");
byte fileByte[] = Files.readAllBytes(Path.of("C:\\dev\\file\\" + storedFileName));
response.setContentType("application/octet-stream");
response.setContentLength(fileByte.length);
response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(originalFileName,"UTF-8")+"\";");
response.getOutputStream().write(fileByte);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
@@ -0,0 +1,147 @@
package kr.itpinc.first.common.dao;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
public class AbstractDAO {
protected Log log = LogFactory.getLog(AbstractDAO.class);
@Autowired
private SqlSessionTemplate sqlSession; /* SqlSessionTemplate 는 context-mapper.xml에 정의됨*/
protected void printQueryId(String queryId) {
if(log.isDebugEnabled()){
log.debug("\t QueryId \t: " + queryId);
}
}
public Object insert(String queryId, Object params){
printQueryId(queryId);
return sqlSession.insert(queryId, params);
}
public Object update(String queryId, Object params){
printQueryId(queryId);
return sqlSession.update(queryId, params);
}
public Object delete(String queryId, Object params){
printQueryId(queryId);
return sqlSession.delete(queryId, params);
}
public Object selectOne(String queryId){
printQueryId(queryId);
return sqlSession.selectOne(queryId);
}
public Object selectOne(String queryId, Object params){
printQueryId(queryId);
return sqlSession.selectOne(queryId, params);
}
@SuppressWarnings("rawtypes")
public List selectList(String queryId){
printQueryId(queryId);
return sqlSession.selectList(queryId);
}
@SuppressWarnings("rawtypes")
public List selectList(String queryId, Object params){
printQueryId(queryId);
return sqlSession.selectList(queryId,params);
}
/*
@SuppressWarnings({ "rawtypes", "unchecked" })
public Map selectPagingList(String queryId, Object params)
{
printQueryId(queryId);
Map<String, Object> map = (Map<String, Object>) params;
PaginationInfo paginationInfo = null;
//containsKey 일치여부
if(map.containsKey("currentPageNo") == false || StringUtils.isEmpty(map.get("currentPageNo")) == true)
map.put("currentPageNo", "1");
paginationInfo = new PaginationInfo();
paginationInfo.setCurrentPageNo(Integer.parseInt(map.get("currentPageNo").toString()));
if(map.containsKey("PAGE_ROW") == false || StringUtils.isEmpty(map.get("PAGE_ROW")) == true)
{
paginationInfo.setRecordCountPerPage(15); //몇개의 행을 보여줄지 셋
}else{
paginationInfo.setRecordCountPerPage(Integer.parseInt(map.get("PAGE_ROW").toString()));
}
paginationInfo.setPageSize(10); //페이지 크기 지정 1~10
int start = paginationInfo.getFirstRecordIndex();
int end = start + paginationInfo.getRecordCountPerPage();
map.put("START",start+1);
map.put("END",end);
params = map;
Map<String,Object> returnMap = new HashMap<String,Object>();
List<Map<String,Object>> list = sqlSession.selectList(queryId,params);
if(list.size() == 0){
map = new HashMap<String,Object>();
map.put("TOTAL_COUNT",0);
list.add(map);
if(paginationInfo != null){
paginationInfo.setTotalRecordCount(0);
returnMap.put("paginationInfo", paginationInfo);
}
}
else{
if(paginationInfo != null){
paginationInfo.setTotalRecordCount(Integer.parseInt(list.get(0).get("TOTAL_COUNT").toString()));
returnMap.put("paginationInfo", paginationInfo);
}
}
returnMap.put("result", list);
return returnMap;
}
*/
@SuppressWarnings("unchecked")
public Object selectPagingList(String queryId, Object params)
{
printQueryId(queryId);
Map<String, Object> map = (Map<String, Object>)params;
// 1. 안전하게 문자로 변환 후 숫자로 파싱 (이전에 수정하신 부분)
int pageIndex = 1; // 기본값 1
int pageRow = 15; // 기본값 15
if(map.containsKey("PAGE_INDEX") && map.get("PAGE_INDEX") != null) {
pageIndex = Integer.parseInt(String.valueOf(map.get("PAGE_INDEX")));
}
if(map.containsKey("PAGE_ROW") && map.get("PAGE_ROW") != null) {
pageRow = Integer.parseInt(String.valueOf(map.get("PAGE_ROW")));
}
// 🌟 핵심 방어 로직: 처음 진입 시 0이 들어오는 것을 방지 🌟
if(pageIndex < 1) {
pageIndex = 1;
}
// 2. 정상적인 START, END 계산
int end = pageIndex * pageRow;
int start = (pageIndex - 1) * pageRow + 1;
map.put("START", start);
map.put("END", end);
return sqlSession.selectList(queryId, map);
}
}
@@ -0,0 +1,17 @@
package kr.itpinc.first.common.dao;
import java.util.Map;
import org.springframework.stereotype.Repository;
@Repository("commonDAO")
public class CommonDAO extends AbstractDAO
{
@SuppressWarnings("unchecked")
public Map<String, Object> selectFileInfo(Map<String, Object> map) throws Exception
{
return (Map<String,Object>)selectOne("common.selectFileInfo", map);
}
}
@@ -0,0 +1,40 @@
package kr.itpinc.first.common.logger;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import kr.itpinc.first.board.controller.BoardController;
@Slf4j // Lombok을 이용한 최신 로깅 (log 변수 자동 생성)
@Aspect
@Component // Spring Bean으로 등록하기 위해 필수
public class LoggerAspect {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoggerAspect.class);
// 패키지 경로를 현재 구조(kr.itpinc.first)에 맞게 보완했습니다.
// 기존 first..service.*Imple.* 오타(Imple -> Impl)도 주의하세요.
@Around("execution(* kr.itpinc.first..controller.*Controller.*(..)) or " +
"execution(* kr.itpinc.first..service.*Impl.*(..)) or " +
"execution(* kr.itpinc.first..dao.*DAO.*(..))")
public Object logPrint(ProceedingJoinPoint joinPoint) throws Throwable {
// 치명적 버그 해결: static이 아닌 메서드 내의 지역 변수로 선언하여 스레드 안전성 확보
String type = joinPoint.getSignature().getDeclaringTypeName();
String name = "";
if (type.contains("Controller")) {
name = "Controller \t: ";
} else if (type.contains("Service")) {
name = "ServiceImpl \t: ";
} else if (type.contains("DAO") || type.contains("Mapper")) {
name = "DAO \t\t: ";
}
// SLF4J의 파라미터화된 로깅 방식 사용 (문자열 더하기 연산보다 성능이 좋음)
log.debug("{}{}.{}()", name, type, joinPoint.getSignature().getName());
return joinPoint.proceed();
}
}
@@ -0,0 +1,38 @@
package kr.itpinc.first.common.logger;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest; // ⚠️ javax -> jakarta 변경 필수
import jakarta.servlet.http.HttpServletResponse; // ⚠️ javax -> jakarta 변경 필수
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.ModelAndView;
@Component
public class LoggerInterceptor implements HandlerInterceptor
{
protected Log log = LogFactory.getLog(LoggerInterceptor.class);
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception
{
if(log.isDebugEnabled())
{
log.debug("================== START ======================");
System.out.println("dddddd");
log.debug(" Request URI \t : "+request.getRequestURI());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception
{
if(log.isDebugEnabled())
{
log.debug("=================== END =======================");
}
}
}
@@ -0,0 +1,50 @@
package kr.itpinc.first.common.resolver;
import jakarta.servlet.http.HttpServletRequest; // 🚨 javax -> jakarta 변경 필수
import kr.itpinc.first.common.common.CommandMap; // 위에서 만든 CommandMap 경로
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.util.Enumeration;
@Component
public class CustomMapArgumentResolver implements HandlerMethodArgumentResolver {
// 1. 이 Resolver가 적용될 파라미터의 타입을 검사합니다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 컨트롤러의 파라미터 타입이 CommandMap 클래스인지 확인
return CommandMap.class.isAssignableFrom(parameter.getParameterType());
}
// 2. 파라미터에 들어갈 실제 객체(데이터)를 생성하여 반환합니다.
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
CommandMap commandMap = new CommandMap();
// 🚨 최신 Jakarta EE 스펙의 HttpServletRequest로 캐스팅
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Enumeration<?> enumeration = request.getParameterNames();
String key;
String[] values;
// request에 담긴 모든 파라미터를 읽어서 commandMap에 집어넣습니다.
while (enumeration.hasMoreElements()) {
key = (String) enumeration.nextElement();
values = request.getParameterValues(key);
if (values != null) {
// 배열의 크기가 1보다 크면(동일한 key로 여러 값이 넘어오면) 배열 자체를 넣고, 아니면 단일 값을 넣습니다.
commandMap.put(key, (values.length > 1) ? values : values[0]);
}
}
return commandMap;
}
}
@@ -0,0 +1,8 @@
package kr.itpinc.first.common.service;
import java.util.Map;
public interface CommonService
{
Map<String, Object> selectFileInfo(Map<String, Object> map) throws Exception;
}
@@ -0,0 +1,26 @@
package kr.itpinc.first.common.service;
import java.util.Map;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import kr.itpinc.first.common.dao.CommonDAO;
import lombok.RequiredArgsConstructor;
@Service("commonService")
public class CommonServiceImpl implements CommonService
{
@Autowired
private CommonDAO commonDAO;
@Override
public Map<String, Object> selectFileInfo(Map<String, Object> map) throws Exception
{
return commonDAO.selectFileInfo(map);
}
}
@@ -0,0 +1,11 @@
package kr.itpinc.first.common.util;
import java.util.UUID;
public class CommonUtils
{
public static String getRandomString()
{
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
@@ -0,0 +1,94 @@
package kr.itpinc.first.common.util;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
@Component("fileUtils")
public class FileUtils {
// 1. Path.of()를 사용하여 OS에 구애받지 않는 안전한 기본 경로 상수로 선언
private static final Path FILE_DIR = Path.of("c:", "dev", "file");
public List<Map<String, Object>> parseInsertFileInfo(Map<String, Object> map, HttpServletRequest request) throws Exception {
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
MultipartFile multipartFile = null;
String originalFileName = null;
String originalFileExtension = null;
String storedFileName = null;
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> listMap = null;
// 데이터 타입 안정성을 위해 String.valueOf 사용
String boardIdx = String.valueOf(map.get("IDX"));
String requestName = null;
String idx = null;
// 2. 주석 처리되었던 디렉토리 생성 로직 복원 (NIO 방식)
// Files.createDirectories는 중간 경로가 없으면 모두 알아서 생성해주며, 이미 존재해도 에러가 나지 않습니다.
if (Files.notExists(FILE_DIR)) {
Files.createDirectories(FILE_DIR);
}
while (iterator.hasNext()) {
multipartFile = multipartHttpServletRequest.getFile(iterator.next());
// multipartFile null 체크 추가 (안정성 강화)
if (multipartFile != null && !multipartFile.isEmpty()) {
originalFileName = multipartFile.getOriginalFilename();
// 확장자가 없는 파일이 업로드될 경우의 StringIndexOutOfBoundsException 방지
if (originalFileName != null && originalFileName.contains(".")) {
originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
} else {
originalFileExtension = "";
}
storedFileName = CommonUtils.getRandomString() + originalFileExtension;
// 3. Path.resolve()를 이용해 상위 경로와 파일명을 결합
Path targetPath = FILE_DIR.resolve(storedFileName);
// 4. Spring 5.1 이상부터 지원하는 transferTo(Path) 사용
multipartFile.transferTo(targetPath);
listMap = new HashMap<>();
listMap.put("IS_NEW", "Y");
listMap.put("BOARD_IDX", boardIdx);
listMap.put("ORIGINAL_FILE_NAME", originalFileName);
listMap.put("STORED_FILE_NAME", storedFileName);
listMap.put("FILE_SIZE", multipartFile.getSize());
list.add(listMap);
} else if (multipartFile != null) {
requestName = multipartFile.getName();
int underscoreIndex = requestName.indexOf("_");
if (underscoreIndex > -1) {
idx = "IDX_" + requestName.substring(underscoreIndex + 1);
if (map.containsKey(idx) && map.get(idx) != null) {
listMap = new HashMap<>();
listMap.put("IS_NEW", "N");
listMap.put("FILE_IDX", map.get(idx));
list.add(listMap);
}
}
}
}
return list;
}
}
@@ -0,0 +1,34 @@
package kr.itpinc.first.config;
import kr.itpinc.first.common.logger.LoggerInterceptor;
import kr.itpinc.first.common.resolver.CustomMapArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
@EnableSpringDataWebSupport
public class WebMvcConfig implements WebMvcConfigurer {
private final LoggerInterceptor loggerInterceptor;
private final CustomMapArgumentResolver customMapArgumentResolver;
// 인터셉터 등록 (Source 2의 mvc:interceptors 대체)
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggerInterceptor)
.addPathPatterns("/**"); // 모든 경로에 적용
}
// Argument Resolver 등록 (Source 2의 mvc:argument-resolvers 대체)
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(customMapArgumentResolver);
}
}
+24
View File
@@ -0,0 +1,24 @@
spring:
# 1. 데이터베이스 설정 (Source 5)
datasource:
# log4jdbc를 사용하는 경우 driver와 url 설정이 아래와 같이 변경됩니다.
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@192.168.31.2:1521:xe
username: scott
password: tiger
# 2. 파일 업로드 제한 설정 (Source 4의 100000000 바이트를 100MB로 직관적 변환)
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 3. JSP 뷰 리졸버 설정 (Source 2)
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
# 4. MyBatis 설정 (Source 6)
mybatis:
mapper-locations: classpath:/mapper/**/*_SQL.xml
@@ -0,0 +1 @@
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
@@ -0,0 +1,40 @@
<?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="common">
<select id="selectFileInfo" parameterType="hashmap" resultType="hashmap">
<![CDATA[
SELECT
STORED_FILE_NAME,
ORIGINAL_FILE_NAME
FROM
TB_FILE
WHERE
IDX=#{IDX}
]]>
</select>
<sql id="pagingPre">
<![CDATA[
SELECT
AAA.*
FROM(
SELECT
COUNT(*) OVER() AS TOTAL_COUNT,
AA.*
FROM(
]]>
</sql>
<sql id="pagingPost">
<![CDATA[
) AA
) AAA
WHERE
AAA.RNUM BETWEEN #{START} AND #{END}
]]>
</sql>
</mapper>