본 시스템은 SpringBoot, Vue, uni-app을 핵심 기술 스택으로 하여 홈케어 플랫폼을 구축합니다. 관리자, 케어사, 노인 사용자 등 다양한 역할에 맞춰 서로 다른 기능과 인터페이스를 제공합니다. 백엔드는 SpringBoot의 강력한 생태계를 활용하여 안정적인 API와 데이터 관리를 보장하고, 프런트엔드는 Vue의 반응형 데이터 바인딩과 컴포넌트 기반 개발 방식을 채택하여 유연하고 효율적인 사용자 경험을 제공합니다. uni-app을 사용하여 웹, 모바일 앱(iOS/Android), 미니 프로그램 등 다양한 플랫폼을 하나의 코드베이스로 지원합니다.
기술 스택 개요
백엔드: Spring Boot
Spring Boot는 내장 서버(Tomcat, Jetty, Undertow)를 제공하여 별도의 설정 없이 애플리케이션을 실행할 수 있습니다. 자동 구성(Auto-Configuration) 기능은 프로젝트 클래스패스의 의존성을 기반으로 애플리케이션을 자동으로 설정해줍니다. Spring Data JPA, Spring Security, Spring Cloud 등 다양한 플러그인과 기능을 제공하여 개발 생산성을 높이고 시스템 확장성을 용이하게 합니다.
프런트엔드: Vue.js
Vue.js는 가상 DOM(Virtual DOM) 기술을 기반으로 효율적인 DOM 조작을 수행합니다. 반응형 데이터 바인딩과 컴포넌트 기반 아키텍처를 통해 데이터 변경 시 UI가 자동으로 업데이트됩니다. 이는 개발자가 UI 업데이트 로직보다 데이터 처리에 집중할 수 있게 하여 코드의 간결성과 유지보수성을 향상시킵니다.
데이터 액세스 계층: MyBatis-Plus
MyBatis-Plus는 MyBatis를 확장한 ORM(Object-Relational Mapping) 프레임워크로, MySQL, Oracle, SQL Server 등 다양한 데이터베이스를 지원합니다. 기본적인 CRUD, 조건 쿼리, 페이징, 코드 생성기 등을 제공하여 반복적인 SQL 작성 및 매핑 작업을 최소화합니다. 또한 낙관적 락(Optimistic Lock), 성능 분석 등 고급 기능을 지원합니다.
시스템 테스트
시스템 품질 및 신뢰성을 보장하기 위해 테스트는 개발 주기에서 중요한 단계입니다. 본 시스템은 주로 블랙박스 테스트(Black-Box Testing)를 수행하여 사용자 관점에서 기능과 로직을 검증합니다.
테스트 목적
다양한 시나리오(입력값 경계값, 필수 입력 누락, 권한별 접근 등)를 시뮬레이션하여 시스템의 잠재적 결함을 발견하고 수정합니다. 모든 테스트는 고객 요구사항 명세서(요구사항 정의서)를 기준으로 하여 실제 사용 환경에서 문제가 발생하지 않도록 하는 것이 목표입니다.
테스트 사례 (예시)
| 기능 | 입력 데이터 | 예상 결과 | 실제 결과 |
|---|---|---|---|
| 로그인 | 사용자명: admin / 비밀번호: 123456 / 캡챠: 정확 | 시스템 로그인 성공 | 성공 |
| 로그인 | 사용자명: admin / 비밀번호: 111111 / 캡챠: 정확 | 비밀번호 오류 메시지 출력 | "비밀번호가 올바르지 않습니다" 출력 |
| 로그인 | 사용자명: 공백 / 비밀번호: 123456 / 캡챠: 정확 | 사용자명 필수 입력 메시지 출력 | "사용자명을 입력해주세요" 출력 |
| 사용자 관리 - 추가 | 모든 필드 정확히 입력 | 사용자 목록에 새 사용자 추가됨 | 성공적으로 추가됨 |
| 사용자 관리 - 추가 | 기존 사용자명 입력 | 중복 경고 메시지 출력 | "사용자명이 이미 존재합니다" 출력 |
| 사용자 관리 - 삭제 | 사용자 선택 후 삭제 버튼 클릭 | 삭제 확인 대화상자 표시 후 사용자 제거 | 확인 후 사용자 목록에서 사라짐 |
테스트 결론
블랙박스 테스트를 통해 모든 주요 기능(로그인, 사용자 CRUD, 권한 검증 등)이 요구사항을 충족하며 정상 작동함을 확인했습니다. 시스템은 의도한 대로 동작하며, 사용자에게 친숙하고 안정적인 환경을 제공합니다.
핵심 백엔드 코드 예시 (Token 인증)
// 로그인 API (인증 불필요)
@IgnoreAuth
@PostMapping(value = "/login")
public R login(String username, String password, String captcha, HttpServletRequest request) {
UsersEntity user = userService.getOne(
new QueryWrapper<UsersEntity>().eq("username", username)
);
if(user == null || !user.getPassword().equals(password)) {
return R.error("아이디 또는 비밀번호가 일치하지 않습니다.");
}
// Token 발급
String token = tokenService.createNewToken(user.getId(), username, "users", user.getRole());
return R.ok().put("token", token);
}
// Token 생성 및 관리 서비스
@Service
public class TokenServiceImpl implements TokenService {
@Override
public String createNewToken(Long userId, String username, String tableName, String role) {
TokenEntity tokenEntity = this.getOne(
new QueryWrapper<TokenEntity>().eq("userid", userId).eq("role", role)
);
String newToken = CommonUtil.generateRandomString(32);
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.HOUR_OF_DAY, 2); // 2시간 유효
if(tokenEntity != null) {
tokenEntity.setToken(newToken);
tokenEntity.setExpirationTime(calendar.getTime());
this.updateById(tokenEntity);
} else {
this.save(new TokenEntity(userId, username, tableName, role, newToken, calendar.getTime()));
}
return newToken;
}
}
// Token 검증 인터셉터
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
public static final String AUTH_HEADER = "Authorization";
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// CORS 설정
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// OPTIONS 요청 처리 (Preflight)
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return false;
}
// @IgnoreAuth 어노테이션 확인
if (handler instanceof HandlerMethod) {
IgnoreAuth annotation = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);
if (annotation != null) {
return true; // 인증 불필요한 요청은 통과
}
} else {
return true;
}
// 헤더에서 Token 추출
String token = request.getHeader(AUTH_HEADER);
TokenEntity tokenEntity = null;
if (StringUtils.hasText(token)) {
tokenEntity = tokenService.getTokenByValue(token);
}
// Token 존재 및 유효성 확인
if (tokenEntity != null && tokenEntity.getExpirationTime().after(new Date())) {
request.getSession().setAttribute("userId", tokenEntity.getUserid());
request.getSession().setAttribute("role", tokenEntity.getRole());
request.getSession().setAttribute("tableName", tokenEntity.getTablename());
request.getSession().setAttribute("username", tokenEntity.getUsername());
return true;
}
// 인증 실패 처리
response.setContentType("application/json; charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.print(JSONObject.toJSONString(R.error(401, "로그인이 필요합니다.")));
writer.close();
return false;
}
}
데이터베이스 테이블 구조 (Token)
-- Token 정보 저장 테이블
DROP TABLE IF EXISTS `auth_token`;
CREATE TABLE `auth_token` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'PK',
`user_id` bigint(20) NOT NULL COMMENT '사용자 ID',
`username` varchar(100) NOT NULL COMMENT '사용자명',
`table_name` varchar(100) DEFAULT NULL COMMENT '관련 테이블명',
`role` varchar(100) DEFAULT NULL COMMENT '역할',
`token_value` varchar(200) NOT NULL COMMENT '토큰 값',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
`expires_at` timestamp NOT NULL COMMENT '만료 시간',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='인증 토큰 테이블';
-- 샘플 데이터
INSERT INTO `auth_token` (`user_id`, `username`, `table_name`, `role`, `token_value`, `expires_at`) VALUES
(1, 'admin', 'users', 'ADMIN', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '2025-01-01 10:00:00');
소스 코드 및 전체 프로젝트는 문의해 주시기 바랍니다.
관련 추천 프로젝트: SpringBoot 기반 실무 프로젝트 500선, Vue/uni-app 기반 미니 프로그램 프로젝트 500선.