I T H

[포트폴리오 프로젝트 3] 관리자 페이지 구현 - 프로필 업데이트 본문

Spring MyPortfolio Project

[포트폴리오 프로젝트 3] 관리자 페이지 구현 - 프로필 업데이트

thdev 2024. 1. 24. 10:18
- 이번에는 관리자 페이지를 통해 메인 페이지의 프로필 이미지 및 프로필 정보를 등록하고 삭제 및 관리할 수 있는 화면을 구현하고자 한다.
- 해당 장은 연습 프로젝트 때 이미지 사용한 것으로 관리자 페이지 구현이 완료된 경우에는 관리자 페이지 구현 부분은 패스하고 하단의 메인 페이지에 이미지 및 프로필 조회만 처리하는 것으로 진행해도 된다.

[ 테이블 생성 ]

- 프로필 정보를 저장하기 위한 데이터베이스 테이블을 1개 생성한다.

CREATE TABLE `TBL_PROFILE_INFO` (
  `PROFILE_ID` varchar(10) NOT NULL,
  `PROFILE_NAME` varchar(20) NOT NULL,
  `PROFILE_JOB` varchar(100) NOT NULL,
  `PROFILE_EMAIL` varchar(100) DEFAULT NULL,
  `PROFILE_IMG` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`PROFILE_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

[ Dependency 추가 ]

- 파일 업로드를 위한 라이브러리를 참조하기 위해 pom.xml 파일에 아래 내용을 추가한다.

참고 > https://commons.apache.org/proper/commons-fileupload/

 

FileUpload – Home

Commons FileUpload The Commons FileUpload package makes it easy to add robust, high-performance, file upload capability to your servlets and web applications. FileUpload parses HTTP requests which conform to RFC 1867, "Form-based File Upload in HTML". That

commons.apache.org

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.1</version>
		</dependency>

[ JavaConfig 설정 ]

-   패키지 아래에 ServletConfig클래스 파일 내용을 수정한다.

-   멀티파트 리졸버 빈 등록 ( public MultipartResolver multipartResolver() )

package kr.co.values.init;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

/**
 * servlet-context.xml 파일을 대신하는 클래스 
 * @ComponentScan 을 통하여 컨트롤러 클래스가 존재하는 패키지를 자동 스캔한다.
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "kr.co.values.*.web" } ) // 수정됨
public class ServletConfig implements WebMvcConfigurer {
	private final int MAX_SIZE = 10 * 1024 * 1024;
	
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		InternalResourceViewResolver bean = new InternalResourceViewResolver();
		bean.setViewClass(JstlView.class);
		bean.setPrefix("/WEB-INF/views/");
		bean.setSuffix(".jsp");
		registry.viewResolver(bean);
	}

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
	
	@Bean
	public MultipartResolver multipartResolver() {
	    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
	    multipartResolver.setMaxUploadSize(MAX_SIZE); // 10MB
	    multipartResolver.setMaxUploadSizePerFile(MAX_SIZE); // 10MB
	    multipartResolver.setMaxInMemorySize(0);
	    return multipartResolver;
	}

}

 

[환경 설정 내용수정]

- kr.co.values/init 패키지에 RootConfig.java, ServletConfig.java 내용을 수정해준다.

[RootConfig.java]

- kr.co.values.admin.persistence or kr.co.value.main.persistence처럼 패키지를 주제에 맞게 나누고 해당 패키지에 대한 persistence를 스캔하기 위해 내용을 수정함.

@MapperScan(basePackages= {"kr.co.values.*.persistence"})

[ServletConfig.java]

@ComponentScan(basePackages = { "kr.co.values.*.web" })

 

-  다음으로는 관리자용 페이지를 하나 생성한다. JSP~컨트롤러까지 admin 이라는 이름으로 한 세트를 작성한다.

[ JSP ]

- webapp/WEB-INF/views/main/admin.jsp 파일 생성한다.

- 파일 1개에 대한 이미지 업로드를 진행하기 위해 file 태그가 포함된 구문을 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
</head>
<body>
	<p>ADMIN PAGE</p>
	<label>프로필 이미지</label>
	<input id="file" name="file" type="file" style="padding: 5pt;">
	<label>프로필 이름</label>
	<input type="text" id="profileName">
	<label>프로필 직업</label>
	<input type="text" id="profileJob">
	<label>프로필 이메일</label>
	<input type="text" id="profileEmail">
	<button id="btnRegister">프로필 등록</button>
<!-- page script -->
<script src="/resources/views/admin.js"></script>
</body>
</html>

[ 이벤트 처리 ]

- resources/views/admin.js 파일을 만들고 아래 스크립트 내용을 작성한다.

- 프로필 정보를 저장할 때 이미지를 등록한 경우에는 이미지(파일) 업로드를 먼저 진행한다.

- 반대로 이미지가 등록되지 않은 경우에는 정보 업데이트만 진행한다.


	$("#btnRegister").on("click", function() {
		
		var profileName = $("#profileName").val();
		var profileJob = $("#profileJob").val();
		var profileEmail = $("#profileEmail").val();
		
		if(profileName == "") {
			alert("프로필 이름을 입력하시기 바랍니다.");
			return;
		}
		
		var obj = {
				profileName: profileName,
				profileJob: profileJob,
				profileEmail: profileEmail,
				profileImg: ""
		}
		
		// 프로필 저장 시에는 아래와 같은 순서로 처리한다.
		// 1. 프로필 이미지를 포함하여 등록 버튼을 누른 경우에는 이미지를 먼저 업로드하고 (업로트 컨트롤러 호출) 저장된 이미지명을 전달받은 후 
		// 전달받은 이미지명을 파라미터에 포함한 뒤 
		// 2. 프로필 정보를 저장한다. 
		// 서버사이드에서는 프로필 정보가 이미 저장되어 있는지 체크 후 insert 혹은 update로 프로플 정보를 저장하게 된다.
		
		var fileImg = $('#file').val();
		
		if(fileImg == "" || fileImg == null) { // 이미지가 포함되지 않은 경우 
			$.ajax({
		        type: "POST",
		        url: "/admin/updateProfile.do",
		        contentType: "application/json;charset=UTF-8",
		        data: JSON.stringify(obj),
		        dataType: "json",
		        success: function(data) {
		        	alert("프로필 저장 성공!");
		        },
		        error: function() {
		        	alert("프로필 저장 실패!!");
		        }
	      	});
		} else {
			var ext = $('#file').val().split('.').pop().toLowerCase();
		    if($.inArray(ext, ['gif','png','jpg','jpeg']) == -1) {
		    	alert('gif, png, jpg, jpeg 파일만 업로드 할수 있습니다.');
				return false;
		    }
		    
			var formData = new FormData();
			formData.append("file", $("#file")[0].files[0]);	//파일 한개
			formData.append("params", JSON.stringify(obj));
			formData.append("enctype", "multipart/form-data");
			
			$.ajax({
				url: "/admin/profileUpload.do",
				type: "POST",
				processData: false,  // file전송시 필수
				contentType: false,  // file전송시 필수
				data: formData,
				dataType: "json",
				success: function(data) {
					// 이미지 등록이 완료되면 서버 사이드에서 전달해주는 src 키에 매핑된 파일명 값을 obj 파라미터에 담은 후 
					// 프로필 정보를 저장하는 컨트롤러를 호출한다.
					console.log(data);
					obj.profileImg = data.src;
					
					$.ajax({
				        type: "POST",
				        url: "/admin/updateProfile.do",
				        contentType: "application/json;charset=UTF-8",
				        data: JSON.stringify(obj),
				        dataType: "json",
				        success: function(data) {
				        	alert("프로필 저장 성공!");
				        },
				        error: function() {
				        	alert("프로필 저장 실패!!");
				        }
			      	});
					
				}, 
				error : function(request){
					console.log(request);
				}
			});
		}
		
	});

[ 컨트롤러 구현 ]

- kr\co\values\admin\web\AdminController.java 파일을 생성하여 아래 내용을 입력한다.

<참고>

transferTo() : 파일을 저장한다

getOriginalFilename() : 파일 이름을 String 값으로 반환한다

getSize() : 파일 크기를 반환한다

getInputStream() : 파일에 대한 입력 스트림을 얻어온다

package kr.co.values.admin.web;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileUploadException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
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.multipart.MultipartFile;

import kr.co.values.admin.service.AdminService;


@Controller
public class AdminController {
	
	@Autowired
	private AdminService adminService;
	
	
    @RequestMapping("/admin/main.do")
    public String main() {
    	
        return "main/admin";
    }
    
    // 프로필 정보 저장
    @RequestMapping("/admin/updateProfile.do")
    @ResponseBody
    public Map<String, Object> updateProfile(@RequestBody Map<String, Object> params) {
    	
    	adminService.updateProfile(params);
    	Map<String, Object> result = new HashMap<String, Object>();
    	result.put("result", true);
    	
        return result;
    }
    
    /**
	 * 업로드 (프로필 이미지 파일 업로드)
	 * for Servlet2.5 Multipart FileUpload
	 * @throws FileUploadException 
	 */
	@RequestMapping("/admin/profileUpload.do")
	@ResponseBody
	public Map<String, Object> profileUpload(@RequestParam("file") MultipartFile part, HttpServletRequest request) throws FileUploadException {
		
		ServletContext context = request.getSession().getServletContext();
		String path = context.getRealPath("/resources/images/profile");

		System.out.println("path : " + path);
		String fileName = part.getOriginalFilename();
		
		System.out.println("fileName : " + fileName);

		InputStream in = null;

		SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
		Date time = new Date();
		String forTime = format.format(time);
		
		int pos = fileName.lastIndexOf(".");
		String ext = fileName.substring(pos + 1);

		try {
			File f = new File(path + "/" + forTime + "." + ext);
			part.transferTo(f);

		} catch (Exception e) {

			throw new FileUploadException(); // 파일업로드를 실패하였습니다.
		} finally {
			if (in != null)
				try {
					in.close();
				} catch (IOException e) {
				}
		}
		
		Map<String, Object> result = new HashMap<String, Object>();
		result.put("src", forTime + "." + ext);

		return result;
    }
    

}

[ 서비스 구현 ]

- kr\co\values\admin\service\AdminService.java 파일을 생성한다.

package kr.co.values.admin.service;

import java.util.Map;

public interface AdminService {
	
	// 프로필 정보 저장
	void updateProfile(Map<String, Object> map);
	
}

 

서비스 단에서 이미 저장된 프로필 정보가 있는 경우를 체크하여 없으면 insert를 진행하고

반대라면 update를 진행하게 되도록 로직을 구현한다.

- kr\co\values\admin\service\AdminServiceImpl.java 파일을 생성한다.

package kr.co.values.admin.service;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.values.admin.persistence.AdminMapper;

@Service
public class AdminServiceImpl implements AdminService {

	@Autowired
	private AdminMapper adminMapper;

	@Override
	public void updateProfile(Map<String, Object> map) {
		
		// 1. 프로필 정보를 저장하기 전에 프로필 테이블에 이미 저장된 정보가 있는지 체크한다.
		List<Map<String, Object>> list = adminMapper.searchProfile(map);
		
		if(list.size() > 0) { // 정보가 있는 경우에는 업데이트로 프로필 정보를 변경
			map.put("profileId", list.get(0).get("PROFILE_ID"));
			adminMapper.updateProfile(map);
		}
		else { // 정보가 없는 경우에는 인서트로 프로필 정보를 최초 저장 
			adminMapper.insertProfile(map);
		}
		
		
	}

	
}

[ 매퍼 구현 ]

- kr\co\values\admin\persistence\AdminMapper.java 파일을 생성한다.

package kr.co.values.admin.persistence;

import java.util.List;
import java.util.Map;

public interface AdminMapper {
	
	// 프로필 저장 정보 조회 
	public List<Map<String, Object>> searchProfile(Map<String, Object> map);
	
	// 프로필 정보 저장 
	public void insertProfile(Map<String, Object> map);
	
	// 프로필 정보 업데이트 
	public void updateProfile(Map<String, Object> map);
}

 

- kr\co\values\admin\persistence\AdminMapper.xml 파일을 생성한다.

<?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="kr.co.values.admin.persistence.AdminMapper"> 
	
	<!-- 프로필 정보 조회  -->
	<select id="searchProfile" parameterType="hashmap" resultType="hashmap">
		SELECT *
		  FROM TBL_PROFILE_INFO
	 	 WHERE 1=1
	</select>
	
	<!-- 프로필 최초 저장 -->
	<insert id="insertProfile" parameterType="hashmap">
		INSERT INTO TBL_PROFILE_INFO (
			  PROFILE_ID
			, PROFILE_NAME
			, PROFILE_JOB
			, PROFILE_EMAIL
			, PROFILE_IMG
		)
		VALUES (
			  'profile'
			, #{profileName}
			, #{profileJob}
			, #{profileEmail}
			, #{profileImg}
		)
	</insert>
	
	<!-- 프로필 정보 업데이트 -->
	<update id="updateProfile" parameterType="hashmap">
		UPDATE TBL_PROFILE_INFO 
		   SET PROFILE_NAME = #{profileName}
			 , PROFILE_JOB = #{profileJob}
			 , PROFILE_EMAIL = #{profileEmail}
			 , PROFILE_IMG = #{profileImg}
		 WHERE PROFILE_ID = #{profileId}
	</update>
	
</mapper>

[ 실행 결과 ]

- /admin/main.do 페이지로 접속 후 기능 테스트


관리자 페이지를 통해서 프로필 정보를 등록하는 기능이 추가된 이후
메인 페이지 상단에 프로필 정보를 출력하는 기능도 추가해준다.

[ 메인 JSP 파일 수정 ] - index.jsp

메인 페이지 상단에 프로필 정보를 출력하는 태그 부분을 아래처럼 수정한다.

각 태그들에 id 를 매핑하고 프로필 정보를 조회 후 id 를 통해 값들을 출력할 것이다.

<h1 id="logo" class="text-center">
    <img id="profileImg" class="img-circle" src="" alt="">
    <span id="profileName" class="title"> </span>
    <span id="profileJob" class="tagline"> </span>
    <span class="tagline"><a id="profileEmail" href=""> </a></span>
</h1>

[ resources / js / index.js 파일 내 스크립트 추가 ]

- 메인페이지가 로드될 때 프로필 정보를 조회하여 프로필 정보를 출력해준다.

// 프로필 정보를 조회하여 화면에 출력한다.
	var obj = {};
	$.ajax({
        type: "POST",
        url: "/main/searchProfile.do",
        contentType: "application/json;charset=UTF-8",
        data: JSON.stringify(obj),
        dataType: "json",
        success: function(data) {
        	console.log(data);
        	$("#profileImg").attr("src", "/resources/images/profile/" + data[0].PROFILE_IMG);
        	$("#profileName").html(data[0].PROFILE_NAME);
        	$("#profileJob").html(data[0].PROFILE_JOB);
        	$("#profileEmail").html(data[0].PROFILE_EMAIL);
        },
        error: function() {
          	console.log('search 통신실패!!');
        }
    });

[ 메인 컨트롤러 수정 ] - kr.co.values/main/web/MainController.java

- 메인 컨트롤러에 프로필 정보 조회용 메소드를 추가하여 준다.

// 프로필 이미지 조회
    @RequestMapping("/main/searchProfile.do")
    @ResponseBody
    public List<Map<String, Object>> searchProfile(@RequestBody Map<String, Object> params) {
    	
    	List<Map<String, Object>> list = mainMapper.searchProfile(params);
    	
        return list;
    }

[ 메인 매퍼 수정 ] - kr.co.values/main/persistence/MainMapper.java

- 조회용 매퍼 인터페이스 메소드 및 쿼리를 작성하여 준다.

// 프로필 조회 
	public List<Map<String, Object>> searchProfile(Map<String, Object> map);

[MainMapper.xml]

<!-- 프로필 조회  -->
	<select id="searchProfile" parameterType="hashmap" resultType="hashmap">
		SELECT *
		  FROM TBL_PROFILE_INFO
	 	 WHERE 1=1
	</select>

[ 실행 결과 ]

- 메인 페이지 접속 시 상단에 프로필 정보가 출력이 되면 정상 동작!