Java 기반 SpringBoot/SSM + Vue + uniapp 여행 관리 시스템 상세 설계 및 구현 (소스코드 + LW + 배포 문서 + 설명 등)

기술 스택

백엔드 프레임워크 SpringBoot

Spring Boot는 Tomcat, Jetty, Undertow 등의 서버를 내장하고 있어 추가 설치 및 설정 없이 바로 사용할 수 있습니다. Spring Boot의 주요 장점은 자동 구성 기능입니다. 프로젝트의 의존성에 따라 애플리케이션을 자동으로 구성할 수 있어 매우 쉽게 애플리케이션을 설정할 수 있습니다. Spring Boot는 Spring Data, Spring Security, Spring Cloud 등과 같은 다양한 기능과 플러그인을 제공합니다. 이러한 기능들은 개발자가 더 빠르게 애플리케이션을 구축하고 다른 기술과의 확장 및 통합을 더 쉽게 할 수 있게 해줍니다. 매우 인기 있는 프레임워크로, 자동 구성, 내장 서버 및 플러그인 기능을 통해 개발자가 더 빠르고 쉽게 고품질의 애플리케이션을 구축할 수 있게 합니다.

프론트엔드 프레임워크 Vue

Vue.js의 핵심은 가상 DOM 기술입니다. 가상 DOM은 메모리 내 데이터 구조로, Vue.js가 효율적인 DOM 조작을 구현하는 데 도움이 됩니다. 반응형 데이터 바인딩, 가상 DOM, 컴포넌트화 등의 현대적인 기술을 채택하여 개발자에게 유연하고 효율적이며 유지보수가 용이한 개발 모드를 제공합니다. 데이터가 변경되면 UI도 자동으로 업데이트되어 개발자는 UI를 수동으로 업데이트하는 대신 데이터 처리에 더 집중할 수 있습니다. 이것이 Vue가 보여주는 간결함, 유연성, 효율성입니다.

영속성 프레임워크 MyBatisPlus

MyBatis-Plus는 MyBatis 프레임워크 기반의 강화 도구로, MyBatis 개발을 단순화하는 것을 목표로 합니다. 오픈소스 Java 프레임워크로 MySQL, Oracle, SQL Server, PostgreSQL 등 다양한 데이터베이스를 지원합니다. MyBatis-Plus는 풍부한 API와 어노테이션을 제공하여 간단한 구성과 사용만으로 ORM 작업을 수행할 수 있어 수동으로 SQL을 작성하는 작업량을 크게 줄여줍니다. 또한, MyBatis-Plus는 코드 생성기를 제공하여 엔티티 클래스, Mapper 인터페이스 및 XML 매핑 파일을 자동으로 생성하여 개발 프로세스를 크게 단순화합니다.

MyBatis-Plus는 페이징 쿼리, 동적 쿼리, 낙관적 잠금, 성능 분석 등의 실용적인 기능을 지원하여 개발자가 효율적인 데이터 조작을 수행할 수 있도록 합니다. MyBatis-Plus를 통해 개발자는 고품질의 데이터 액세스 계층 코드를 빠르게 개발하고 개발 효율성을 높일 수 있습니다.

시스템 테스트

여러 관점에서 테스트를 수행하여 시스템에 존재하는 문제를 찾는 것이 이 시스템의 주요 테스트 목적입니다. 기능 테스트를 통해 시스템 결함을 찾아 수정하여 시스템에 결함이 없도록 보장합니다. 테스트 과정에서 시스템이 고객 요구사항을 충족함을 증명하고 문제와 부족한 점을 발견하여 즉시 수정합니다. 테스트 완료 후 테스트 결론을 도출합니다.

시스템 테스트 목적

호텔 관리 시스템의 개발 주기에서 시스템 테스트는 필수적이며 인내심을 시험하는 과정입니다. 그 중요성은 시스템 품질과 신뢰성을 보장하는 마지막 관문이며, 전체 시스템 개발 과정의 마지막 검사이기 때문입니다. 시스템 테스트의 주요 목적은 사용자가 사용할 때 문제가 발생하지 않도록 하고 사용자 경험을 향상시키는 것입니다. 사용자의 사용에 영향을 주지 않기 위해 시스템이 마주할 수 있는 문제에 대해 다각도, 다양한 사고방식으로 고려해야 합니다. 다양한 시뮬레이션 시나리오를 통해 결함을 발견하고 해결해야 합니다. 테스트 과정에서 해당 시스템의 품질 상황, 시스템 기능이 완전한지, 시스템 로직이 원활한지를 알 수 있습니다. 적격한 시스템 테스트 과정을 완료하면 시스템 품질과 사용감이 크게 향상됩니다. 테스트의 목표는 시스템이 요구사항 명세서의 정의를 충족하는지 확인하고 요구사항 명세서와 부합하지 않거나 충돌하는 내용을 찾는 것입니다. 테스트 과정에서는 항상 사용자의 관점에서 문제를 고려하여 비현실적인 시나리오로 인해 테스트 시간을 낭비하지 않도록 해야 합니다.

시스템 기능 테스트

시스템 기능 모듈을 테스트하며, 클릭, 입력 경계값 및 필수 항목/비필수 항목 확인 등의 방법을 사용하여 일련의 블랙박스 테스트를 수행합니다. 테스트 케이스를 작성하고 테스트 케이스의 내용에 따라 테스트를 수행한 후 최종 테스트 결론을 도출합니다.

로그인 기능 테스트 방안: 시스템에 로그인할 때 계정 비밀번호 등의 기능 포인트를 통해 인증합니다. 사용자는 데이터베이스에 저장된 데이터와 일치하는 내용을 입력해야 합니다. 입력 중 일부가 잘못되면 시스템은 입력 오류를 알립니다. 이 인터페이스는 역할 권한에 대한 해당 검증도 수행하며, 사용자 역할의 계정이 관리자 역할로 로그인하면 오류가 발생합니다. 로그인 기능 테스트 케이스는 다음 표와 같습니다.

입력 데이터 예상 결과 실제 결과 결과 분석
사용자명: 관리자 비밀번호: 123456 인증코드: 정확한 입력 시스템 로그인 시스템 로그인 성공 추정 결과와 동일
사용자명: 관리자 비밀번호: 111111 인증코드: 정확한 입력 비밀번호 오류 비밀번호 오류, 비밀번호를 다시 입력하세요 추정 결과와 동일
사용자명: 관리자 비밀번호: 123456 인증코드: 잘못된 입력 인증코드 오류 인증코드 정보 오류 추정 결과와 동일
사용자명: 공백 비밀번호: 123456 인증코드: 정확한 입력 사용자명 필수 입력 사용자명을 입력하세요 추정 결과와 동일
사용자명: 관리자 비밀번호: 공백 인증코드: 정확한 입력 비밀번호 오류 비밀번호 오류, 비밀번호를 다시 입력하세요 추정 결과와 동일

사용자 관리 기능 테스트 방안: 사용자 관리에는 추가, 편집, 삭제, 사용자 검색 기능이 주로 포함됩니다. 사용자를 추가할 때 필수 항목을 입력하지 않으면 시스템에 비어 있지 않은 검증이 있는지 확인합니다; 기존 사용자 정보를 추가하면 사용자 이름이 사용 중인지 여부를 확인합니다; 사용자 정보를 삭제하면 시스템이 해당 작업을 수행하는지 확인합니다; 사용자 정보를 변경하면 변경된 정보가 페이지에 표시되는지 확인합니다. 사용자 관리 테스트 케이스는 다음 표와 같습니다.

입력 데이터 예상 결과 실제 결과 결과 분석
사용자 기본 정보 입력 추가 성공, 사용자 목록에 표시 해당 사용자가 목록에 표시됨 추정 결과와 동일
사용자 정보 수정 편집 성공, 수정 정보가 성공적으로 수정됨 사용자 정보가 수정됨 추정 결과와 동일
사용자 삭제 선택 시스템이 사용자 삭제 여부를 묻고 확인 후 사용자가 삭제됨 시스템이 사용자 삭제 여부를 묻고 확인 후 사용자 정보를 찾을 수 없음 추정 결과와 동일
사용자 추가 시 사용자명 미입력 사용자명은 비어 있을 수 없음을 표시 사용자명은 비어 있을 수 없음을 표시 추정 결과와 동일
기존 사용자명 입력 추가 실패, 사용자명 중복을 표시 추가 실패, 사용자명 중복을 표시 추정 결과와 동일

시스템 테스트 결론

이 시스템은 주로 블랙박스 테스트를 사용하며, 사용자가 시스템을 사용하는 것을 시뮬레이션하여 각 기능에 대한 테스트 케이스를 작성하고 테스트를 수행합니다. 시스템의 흐름 정확성을 보장하기 위해. 시스템 테스트는 필수적이며 시스템을 더 완벽하게 만들고 시스템의 사용 가능성도 더 높일 수 있습니다.

이 시스템을 테스트하는 주요 목적은 시스템의 기능 모듈이 우리의 초기 설계 개념을 충족하는지 확인하고 각 기능 모듈의 로직이 올바른지 확인하는 것입니다. 이 시스템은 너무 복잡한 로직 처리가 필요하지 않아 사용자가 쉽게 조작할 수 있습니다. 테스트의 최종 목표는 사용자 사용을 중심으로 합니다. 테스트 과정에서 모든 시나리오는 사용자 요구사항을 충족해야 하며 요구사항 목표에서 벗어나서는 안 됩니다. 문제가 발생할 때는 항상 사용자의 관점에서 생각해야 합니다. 일련의 테스트 과정을 거친 후 최종 테스트 결과를 얻을 수 있으며, 테스트 결과에서 볼 수 있듯이 구현된 시스템은 기능 및 성능 면에서 설계 요구사항을 충족합니다.

코드 예시

@SkipAuth
@PostMapping(value = "/auth")
public Response login(String userId, String passwd, String captcha, HttpServletRequest request) {
   UserEntity user = userService.findUser(new QueryWrapper<UserEntity>().eq("user_id", userId));
   if(user==null || !user.getPassword().equals(passwd)) {
      return Response.error("아이디 또는 비밀번호가 올바르지 않습니다");
   }
   String authToken = authService.generateToken(user.getId(),userId, "users", user.getRole());
   return Response.success().put("authToken", authToken);
}

	@Override
	public String generateToken(Long userId,String userName, String tableName, String role) {
		TokenRecord tokenRecord = this.findOne(new QueryWrapper<TokenRecord>().eq("user_id", userId).eq("role", role));
		String newToken = SecurityUtil.generateRandomToken(32);
		Calendar cal = Calendar.getInstance();   
    	cal.setTime(new Date());   
    	cal.add(Calendar.HOUR_OF_DAY, 1);
		if(tokenRecord!=null) {
			tokenRecord.setToken(newToken);
			tokenRecord.setExpiryTime(cal.getTime());
			this.updateById(tokenRecord);
		} else {
			this.save(new TokenRecord(userId,userName, tableName, role, newToken, cal.getTime()));
		}
		return newToken;
	}



/**
 * 권한(Token) 검증
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    public static final String AUTH_TOKEN_HEADER = "Authorization";

    @Autowired
    private AuthService authService;
    
	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

		//CORS 요청 지원
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,request-source,Authorization, Origin,imgType, Content-Type, cache-control,postman-token,Cookie, Accept,authorization");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
	// 크로스 도메인 시 OPTIONS 요청이 먼저 전송되므로 이 요청에 대해 정상 상태 반환
	if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
        	response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        SkipAuth annotation;
        if (handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(SkipAuth.class);
        } else {
            return true;
        }

        //헤더에서 토큰 가져오기
        String token = request.getHeader(AUTH_TOKEN_HEADER);
        
        /**
         * 권한 검증이 필요 없는 메서드는 바로 통과
         */
        if(annotation!=null) {
        	return true;
        }
        
        TokenRecord tokenRecord = null;
        if(StringUtils.isNotBlank(token)) {
        	tokenRecord = authService.getTokenRecord(token);
        }
        
        if(tokenRecord != null) {
        	request.getSession().setAttribute("userId", tokenRecord.getUserId());
        	request.getSession().setAttribute("role", tokenRecord.getRole());
        	request.getSession().setAttribute("tableName", tokenRecord.getTableName());
        	request.getSession().setAttribute("userName", tokenRecord.getUserName());
        	return true;
        }
        
		PrintWriter writer = null;
		response.setCharacterEncoding("UTF-8");
		response.setContentType("application/json; charset=utf-8");
		try {
		    writer = response.getWriter();
		    writer.print(JSONObject.toJSONString(Response.error(401, "로그인이 필요합니다")));
		} finally {
		    if(writer != null){
		        writer.close();
		    }
		}
		return false;
    }
}

데이터베이스 예시

-- ----------------------------
-- Table structure for token_records
-- ----------------------------
DROP TABLE IF EXISTS `token_records`;
CREATE TABLE `token_records` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '기본 키',
  `user_id` bigint(20) NOT NULL COMMENT '사용자 ID',
  `user_name` varchar(100) NOT NULL COMMENT '사용자 이름',
  `table_name` varchar(100) DEFAULT NULL COMMENT '테이블 이름',
  `role` varchar(100) DEFAULT NULL COMMENT '역할',
  `token` varchar(200) NOT NULL COMMENT '토큰',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
  `expiry_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '만료 시간',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='토큰 테이블';

-- ----------------------------
-- Records of token_records
-- ----------------------------
INSERT INTO `token_records` VALUES ('9', '23', 'cd01', 'students', '학생', 'al6svx5qkei1wljry5o1npswhdpqcpcg', '2023-02-23 21:46:45', '2023-03-15 14:01:36');
INSERT INTO `token_records` VALUES ('10', '11', 'xh01', 'students', '학생', 'fahmrd9bkhqy04sq0fzrl4h9m86cu6kx', '2023-02-27 18:33:52', '2023-03-17 18:27:42');
INSERT INTO `token_records` VALUES ('11', '17', 'ch01', 'students', '학생', 'u5km44scxvzuv5yumdah2lhva0gp4393', '2023-02-27 18:46:19', '2023-02-27 19:48:58');
INSERT INTO `token_records` VALUES ('12', '1', 'admin', 'users', '관리자', 'h1pqzsb9bldh93m92j9m2sljy9bt1wdh', '2023-02-27 19:37:01', '2023-03-17 18:23:02');
INSERT INTO `token_records` VALUES ('13', '21', 'xiaohao', 'managers', '관리자', 'zdm7j8h1wnfe27pkxyiuzvxxy27ykl2a', '2023-02-27 19:38:07', '2023-03-17 18:25:20');
INSERT INTO `token_records` VALUES ('14', '27', 'djy01', 'students', '학생', 'g3teq4335pe21nwuwj2sqkrpqoabqomm', '2023-03-15 12:56:17', '2023-03-15 14:00:16');
INSERT INTO `token_records` VALUES ('15', '29', 'dajiyue', 'managers', '관리자', '0vb1x9xn7riewlp5ddma5ro7lp4u8m9j', '2023-03-15 12:58:08', '2023-03-15 14:03:48');

태그: SpringBoot Vue.js UniApp MyBatisPlus java

6월 12일 16:46에 게시됨