diff --git a/src/main/java/kr/itpinc/first/common/common/CommandMap.java b/src/main/java/kr/itpinc/first/common/common/CommandMap.java new file mode 100644 index 0000000..f704554 --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/common/CommandMap.java @@ -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 map = new HashMap(); + + 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> entrySet(){ + return map.entrySet(); + } + + public Set keySet(){ + return map.keySet(); + } + + public boolean isEmpty(){ + return map.isEmpty(); + } + + public void putAll(Map m){ + map.putAll(m); + } + + public Map getMap(){ + return map; + } +} \ No newline at end of file diff --git a/src/main/java/kr/itpinc/first/common/controller/CommonController.java b/src/main/java/kr/itpinc/first/common/controller/CommonController.java new file mode 100644 index 0000000..d80d28b --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/controller/CommonController.java @@ -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 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(); + } +} diff --git a/src/main/java/kr/itpinc/first/common/dao/AbstractDAO.java b/src/main/java/kr/itpinc/first/common/dao/AbstractDAO.java new file mode 100644 index 0000000..47880dc --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/dao/AbstractDAO.java @@ -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 map = (Map) 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 returnMap = new HashMap(); + List> list = sqlSession.selectList(queryId,params); + + if(list.size() == 0){ + map = new HashMap(); + 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 map = (Map)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); + } +} + + diff --git a/src/main/java/kr/itpinc/first/common/dao/CommonDAO.java b/src/main/java/kr/itpinc/first/common/dao/CommonDAO.java new file mode 100644 index 0000000..71b280a --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/dao/CommonDAO.java @@ -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 selectFileInfo(Map map) throws Exception + { + return (Map)selectOne("common.selectFileInfo", map); + } + +} diff --git a/src/main/java/kr/itpinc/first/common/logger/LoggerAspect.java b/src/main/java/kr/itpinc/first/common/logger/LoggerAspect.java new file mode 100644 index 0000000..78cb5a0 --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/logger/LoggerAspect.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/kr/itpinc/first/common/logger/LoggerInterceptor.java b/src/main/java/kr/itpinc/first/common/logger/LoggerInterceptor.java new file mode 100644 index 0000000..af2dc36 --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/logger/LoggerInterceptor.java @@ -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 ======================="); + } + } +} diff --git a/src/main/java/kr/itpinc/first/common/resolver/CustomMapArgumentResolver.java b/src/main/java/kr/itpinc/first/common/resolver/CustomMapArgumentResolver.java new file mode 100644 index 0000000..4a7560d --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/resolver/CustomMapArgumentResolver.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/kr/itpinc/first/common/service/CommonService.java b/src/main/java/kr/itpinc/first/common/service/CommonService.java new file mode 100644 index 0000000..d5e7a90 --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/service/CommonService.java @@ -0,0 +1,8 @@ +package kr.itpinc.first.common.service; + +import java.util.Map; + +public interface CommonService +{ + Map selectFileInfo(Map map) throws Exception; +} diff --git a/src/main/java/kr/itpinc/first/common/service/CommonServiceImpl.java b/src/main/java/kr/itpinc/first/common/service/CommonServiceImpl.java new file mode 100644 index 0000000..6628acc --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/service/CommonServiceImpl.java @@ -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 selectFileInfo(Map map) throws Exception + { + return commonDAO.selectFileInfo(map); + } +} diff --git a/src/main/java/kr/itpinc/first/common/util/CommonUtils.java b/src/main/java/kr/itpinc/first/common/util/CommonUtils.java new file mode 100644 index 0000000..e52b2b9 --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/util/CommonUtils.java @@ -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("-", ""); + } +} diff --git a/src/main/java/kr/itpinc/first/common/util/FileUtils.java b/src/main/java/kr/itpinc/first/common/util/FileUtils.java new file mode 100644 index 0000000..8c3497b --- /dev/null +++ b/src/main/java/kr/itpinc/first/common/util/FileUtils.java @@ -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> parseInsertFileInfo(Map map, HttpServletRequest request) throws Exception { + MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request; + Iterator iterator = multipartHttpServletRequest.getFileNames(); + + MultipartFile multipartFile = null; + String originalFileName = null; + String originalFileExtension = null; + String storedFileName = null; + + List> list = new ArrayList<>(); + Map 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; + } +} \ No newline at end of file diff --git a/src/main/java/kr/itpinc/first/config/WebMvcConfig.java b/src/main/java/kr/itpinc/first/config/WebMvcConfig.java new file mode 100644 index 0000000..4375b42 --- /dev/null +++ b/src/main/java/kr/itpinc/first/config/WebMvcConfig.java @@ -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 resolvers) { + resolvers.add(customMapArgumentResolver); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..71ea221 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/src/main/resources/log4jdbc.log4j2.properties b/src/main/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..551c9ef --- /dev/null +++ b/src/main/resources/log4jdbc.log4j2.properties @@ -0,0 +1 @@ +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator \ No newline at end of file diff --git a/src/main/resources/mapper/common/Common_SQL.xml b/src/main/resources/mapper/common/Common_SQL.xml new file mode 100644 index 0000000..8fe0f95 --- /dev/null +++ b/src/main/resources/mapper/common/Common_SQL.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + \ No newline at end of file